[C#] 在 Visual Studio 2015 使用 C# 7

昨天在 "C# 7 不能編譯?" 一文的最後,我有提到不是一定要 Visual Studio 2017 才能使用 C# 7,只要 Microsoft.Net.Compilers 可以運作 (.NET 4.5.1 以上),就能使用 C# 7 快樂的寫程式,有朋友回應說 Visual Studio 2015 上使用 C# 7 ,Microsoft.Net.Compilers 已經升級到 2.0,但卻無法編譯... (這個問題要解決有個但書)

以下的測試,是基於專案是使用 Visual Studio 2017 所建立的情形下才可以使用。
經過我的初步調查,這個差距是來自於 Microsoft.Common.props 這個檔案,裡面對編譯工具選擇方式的定義有差別,Visual Studio 2015 還是以 Microsoft Build Tools 為基礎,但 Visual Studio 2017 起改為偵測專案目錄下是否有存在 Build Tools (也就是 Microsoft.Net.Compilers 套件所安裝的建造工具),因此用 Visual Studio 2015 開啟 Visual Studio 2017 所建立的專案時,能成功建置 C# 7 語法的程式,但若是用 Visual Studio 2015 新增專案,會因為 Microsoft.Common.props 還是固定使用 Microsoft Build Tools 而無法編譯 C# 7 的語法。

我使用的環境是 Visual Studio 2015 with Update 3,打開 Visual Studio 2017 所建的 Console Application 專案,並且在專案中安裝 Microsoft.Net.Compilers 2.0.1,然後在程式中同時實作 System.ValueTuple 和 inline function,程式如下:

using System;

namespace ConsoleApp3
{
    class Program
    {
        static void Main(string[] args)
        {
            var values = GetRandomValues();
            Console.WriteLine($"{values.x}, {values.y}");
            Console.ReadLine();
        }

        static (int x, int y) GetRandomValues()
        {
            return GetRandomValuesInner();

            (int x, int y) GetRandomValuesInner()
            {
                var rnd = new Random();

                return new ValueTuple<int, int>(rnd.Next(), rnd.Next());
            }
        }
    }
}

不過在編寫的時候,因為 Visual Studio 2015 的 IDE 並不知道 C# 7 的語法特性,所以會看到一堆毛毛蟲。

但是編譯卻是可以過的,有趣吧。

而且還真的可以跑喔。

所以證實了 Visual Studio 不再管理編譯器,只要是安裝了 Microsoft.Net.Compilers 套件,Visual Studio 就會使用 Microsoft.Net.Compilers 裡面的編譯器,而不是用 .NET Framework 本身的。

至於 Visual Studio IDE 上的毛毛蟲有沒有解呢? 我做了簡單的測試,安裝 Microsoft.CodeAnalysis 套件 2.0,但仍然無法消除毛毛蟲。

頂多只是提示中會顯示 C# 7.0 language feature 的提示 (這個是有安裝 Resharper 才會有的提示),不過這不影響編譯。

本文僅就 Visual Studio 2015 測試,至於 Visual Studio 2013 或更早的版本能否運作我就不確定了,因為我也沒有環境可測試。

註:一開始說的 Microsoft.Commons.prop 的改變,Visual Studio 2017 的版本比 Visual Studio 2015 的多了下列這一大段,賦與 Visual Studio 2017 的專案能使用 Microsoft.Net.Compilers 的建置工具編譯。

<!-- 
        Determine the path to the directory build props file if the user did not disable $(ImportDirectoryBuildProps) and
        they did not already specify an absolute path to use via $(DirectoryBuildPropsPath)
    -->
  <PropertyGroup Condition="'$(ImportDirectoryBuildProps)' == 'true' and '$(DirectoryBuildPropsPath)' == ''">
    <_DirectoryBuildPropsFile Condition="'$(_DirectoryBuildPropsFile)' == ''">Directory.Build.props</_DirectoryBuildPropsFile>
    <_DirectoryBuildPropsBasePath Condition="'$(_DirectoryBuildPropsBasePath)' == ''">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), '$(_DirectoryBuildPropsFile)'))</_DirectoryBuildPropsBasePath>
    <DirectoryBuildPropsPath Condition="'$(_DirectoryBuildPropsBasePath)' != '' and '$(_DirectoryBuildPropsFile)' != ''">$([System.IO.Path]::Combine('$(_DirectoryBuildPropsBasePath)', '$(_DirectoryBuildPropsFile)'))</DirectoryBuildPropsPath>
  </PropertyGroup>

  <Import Project="$(DirectoryBuildPropsPath)" Condition="'$(ImportDirectoryBuildProps)' == 'true' and exists('$(DirectoryBuildPropsPath)')"/>

  <!-- 
        Prepare to import project extensions which usually come from packages.  Package management systems will create a file at:
          $(MSBuildProjectExtensionsPath)\$(MSBuildProjectFile).<SomethingUnique>.props
          
        Each package management system should use a unique moniker to avoid collisions.  It is a wild-card import so the package
        management system can write out multiple files but the order of the import is alphabetic because MSBuild sorts the list.
    -->
  <PropertyGroup>
    <!--
            The declaration of $(BaseIntermediateOutputPath) had to be moved up from Microsoft.Common.CurrentVersion.targets
            in order for the $(MSBuildProjectExtensionsPath) to use it as a default.
        -->
    <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">obj\</BaseIntermediateOutputPath>
    <BaseIntermediateOutputPath Condition="!HasTrailingSlash('$(BaseIntermediateOutputPath)')">$(BaseIntermediateOutputPath)\</BaseIntermediateOutputPath>
    <MSBuildProjectExtensionsPath Condition="'$(MSBuildProjectExtensionsPath)' == '' ">$(BaseIntermediateOutputPath)</MSBuildProjectExtensionsPath>
    <!--
        Import paths that are relative default to be relative to the importing file.  However, since MSBuildExtensionsPath
        defaults to BaseIntermediateOutputPath we expect it to be relative to the project directory.  So if the path is relative
        it needs to be made absolute based on the project directory.
      -->
    <MSBuildProjectExtensionsPath Condition="'$([System.IO.Path]::IsPathRooted($(MSBuildProjectExtensionsPath)))' == 'false'">$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(MSBuildProjectExtensionsPath)'))</MSBuildProjectExtensionsPath>
    <MSBuildProjectExtensionsPath Condition="!HasTrailingSlash('$(MSBuildProjectExtensionsPath)')">$(MSBuildProjectExtensionsPath)\</MSBuildProjectExtensionsPath>
    <ImportProjectExtensionProps Condition="'$(ImportProjectExtensionProps)' == ''">true</ImportProjectExtensionProps>
  </PropertyGroup>

  <Import Project="$(MSBuildProjectExtensionsPath)$(MSBuildProjectFile).*.props" Condition="'$(ImportProjectExtensionProps)' == 'true' and exists('$(MSBuildProjectExtensionsPath)')" />