C#

C# 6.0 语言预览版

Mark Michaelis

您阅读本文的时候,Build(Microsoft 开发人员大会)将落下帷幕,开发人员将思考如何应对呈现在面前的这一切:立即接受、等等再看还是暂时忽略。 对于 .NET/C# 开发人员来说,最重要的公告无疑是将 C# 编译器(“Roslyn”)的下一个版本发布为开源版本。 与此相关的是语言改进本身。 即使您未计划立即采用 C# vNext(以下为非正式说法“C# 6.0”),您至少应了解它的功能,并留意那些可能值得您立即投入使用的功能。

本文中,我将深入探讨在撰写本文时(2014 年 3 月)C# 6.0 已提供功能的详细信息,或当前可从roslyn.codeplex.com 下载的开源部分已提供功能的详细信息。 我将称之为单个版本——3 月预览版。 3 月预览版特定于 C# 的功能完全在编译器中实现,与更新的 Microsoft .NET Framework 或运行时不存在任何依赖关系。 这意味着您可以在开发过程中采用 C# 6.0,而无需为开发或部署升级 .NET Framework。 事实上,通过此版本安装 C# 6.0 编译器比安装 Visual Studio 2013 扩展涉及的事项稍多一些,因为还要更新 MSBuild 目标文件。

我在介绍每个 C# 6.0 功能时,您可能要考虑以下几点:

  • 过去是否有合理的方法来编写相同的功能,以致功能主要为语法修饰(快捷方式或简化的方法)? 例如,异常筛选不具备 C# 5.0 等效功能,但主构造函数具备这样的功能。
  • 3 月预览版中是否提供此功能? 我要介绍的大部分功能都提供,但有些功能(如新的二进制文字)不提供。
  • 关于新语言功能,您是否有要向团队提供的任何反馈? 团队仍处于版本生命周期的相对早期阶段,非常愿意倾听您有关此版本的想法(有关反馈说明,请参阅 msdn.com/Roslyn)。

考虑此类问题有助于衡量与您自己的开发工作相关的新功能的意义。

索引成员和元素初始值设定项

首先,请看看图 1 中的单元测试。

图 1 通过集合初始值设定项为集合赋值(在 C# 3.0 中添加)

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
// ...
[TestMethod]
public void DictionaryIndexWithoutDotDollar()
{
  Dictionary<string, string> builtInDataTypes = 
    new Dictionary<string, string>()
  {
    {"Byte", "0 to 255"},
    // ...
    {"Boolean", "True or false."},
    {"Object", "An Object."},
    {"String", "A string of Unicode characters."},
    {"Decimal", "±1.0 × 10e-28 to ±7.9 × 10e28"}
  };
  Assert.AreEqual("True or false.", builtInDataTypes["Boolean"]);
}

尽管语法有些模糊不清,但图 1 只不过是一个名称/值集合。 其实语法可以显著地简化为:<index> = <value>。 C# 6.0 通过 C# 对象初始值设定项和新的索引成员语法使之成为可能。 以下代码显示基于整数的元素初始值设定项:

var cppHelloWorldProgram = new Dictionary<int, string>
{
  [10] = "main() {",
  [20] = "    printf(\"hello, world\")",
  [30] = "}"
};
Assert.AreEqual(3, cppHelloWorldProgram.Count);

请注意,尽管此代码为索引使用整数,但 Dictionary<TKey,TValue> 可以支持使用任何类型作为索引(只要其支持 IComparable<T>)。 下一个示例显示了索引数据类型的字符串,并使用索引成员初始值设定项指定元素值:

Dictionary<string, string> builtInDataTypes =
  new Dictionary<string, string> {
    ["Byte"] = "0 to 255",
    // ...
    // Error: mixing object initializers and
    // collection initializers is invalid
    // {" Boolean", "True or false."},
    ["Object"] = "An Object.",
    ["String"] = "A string of Unicode characters.",
    ["Decimal"] = "±1.0 × 10e?28 to ±7.9 × 10e28"
  };

新的索引成员初始化附带了一个新的 $ 运算符。 此字符串索引成员语法专门用于解决基于字符串的索引普遍存在的问题。 使用图 2 中所示的这个新语法,您可以在动态成员调用(C# 4.0 中进行了介绍)中为语法中的元素赋值,而不是在上述示例使用的字符串标记中为其赋值。

图 2 使用索引成员赋值将集合初始化为元素初始值设定项的一部分

[TestMethod]
public void DictionaryIndexWithDotDollar()
{
  Dictionary<string, string> builtInDataTypes = 
    new Dictionary<string, string> {
    $Byte = "0 to 255",   // Using indexed members in element initializers
    // ...
    $Boolean = "True or false.",
    $Object = "An Object.",
    $String = "A string of Unicode characters.",
    $Decimal = "±1.0 × 10e?28 to ±7.9 × 10e28"
  };
  Assert.AreEqual("True or false.", builtInDataTypes.$Boolean);
}

若要了解 $ 运算符,可以看看 AreEqual 函数调用。 请注意 builtInDataTypes 变量上“$Boolean”的 Dictionary 成员调用(即使 Dictionary 上没有“Boolean”成员,也请注意)。 这种显式成员不是必需的,因为 $ 运算符会调用 Dictionary 上的索引成员,相当于调用 buildInDataTypes["Boolean"]。

与任何基于字符串的运算符一样,字符串索引元素(例如,“Boolean”)在 Dictionary 中不进行编译时验证。 因此,任何有效的 C#(区分大小写)成员名称都可以在 $ 运算符之后出现。

为了完全把握索引成员的语法,请考虑松散类型化数据格式(如 XML、JSON 和 CSV)的字符串索引器、甚至是数据库查找(假定没有实体框架代码生成幻数)的优势。 例如,图 3 演示了使用 Newtonsoft.Json 框架的字符串索引成员的便捷性。

图 3 利用采用了 JSON 数据的索引方法

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
// ...
[TestMethod]
public void JsonWithDollarOperatorStringIndexers()
{
  // Additional data types eliminated for elucidation
  string jsonText = @"
    {
      'Byte':  {
        'Keyword':  'byte',
        'DotNetClassName':  'Byte',
        'Description':  'Unsigned integer',
        'Width':  '8',
        'Range':  '0 to 255'
                },
      'Boolean':  {
        'Keyword':  'bool',
        'DotNetClassName':  'Boolean',
        'Description':  'Logical Boolean type',
        'Width':  '8',
        'Range':  'True or false.'
                  },
    }";
  JObject jObject = JObject.Parse(jsonText);
  Assert.AreEqual("bool", jObject.$Boolean.$Keyword);
}

需要注意的最后一点是,为防止其不明显,$ 运算符语法仅使用类型字符串(如 Dictionary<string, …>)的索引。

使用初始化表达式的自动属性

现在初始化类有时会很复杂。 例如,请看自定义集合类型(如 Queue<T>)在内部维护项目列表的私有 System.Collections.Generic.List<T> 属性的简单案例。 实例化集合时,您需要通过队列要包含的项目列表初始化队列。 然而,使用一个属性进行此操作的合理方案需要一个支持字段和一个初始化表达式或一个 else 构造函数,这个组合实际上使所需代码数量翻倍。

C# 6.0 提供了一种快捷语法:自动属性初始化表达式。 现在您可以直接给自动属性赋值,如下所示:

class Queue<T>
{
  private List<T> InternalCollection { get; } = 
    new List<T>; 
  // Queue Implementation
  // ...
}

请注意,在这种情况下,属性为只读(未定义 setter)。 但是,在声明时仍然可以对属性赋值。 还支持定义了 setter 的读/写属性。

主构造函数

类似于属性初始化表达式,C# 6.0 也提供了定义构造函数的快捷语法。 请看看图 4 中所示的 C# 构造函数和属性验证的普及情况。

图 4 常见构造函数模式

[Serializable]
public class Patent
{
  public Patent(string title , string yearOfPublication)
  {
    Title = title;
    YearOfPublication = yearOfPublication;
  }
  public Patent(string title, string yearOfPublication,
    IEnumerable<string> inventors)
    : this(title, yearOfPublication)
  {
    Inventors = new List<string>();
    Inventors.AddRange(inventors);
  }
  [NonSerialized] // For example
  private string _Title;
  public string Title
  {
    get
    {
      return _Title;
    }
    set
    {
      if (value == null)
      {
        throw new ArgumentNullException("Title");
      }
      _Title = value;
    }
  }
  public string YearOfPublication { get; set; }
  public List<string> Inventors { get; private set; }
  public string GetFullName()
  {
    return string.Format("{0} ({1})", Title, YearOfPublication);
  }
}

此常见构造函数模式中有几个需要注意的要点:

  1. 属性需要验证的实际情况促使需要声明基础属性字段。
  2. 常见的公共类 Patent{  public Patent(... 的重复 使构造函数语法有些冗长。
  3. 具有各种区分大小写组合的“Title”在这个非常简单的情景中出现了 7 次(不包括验证)。
  4. 属性的初始化需要对构造函数内的属性进行显式引用。

为了在不降低语言质量的前提下降低此模式的繁琐程度,C# 6.0 引入了属性初始化表达式和主构造函数,如图 5 中所示。

图 5 使用主构造函数

[Serializable]
public class Patent(string title, string yearOfPublication)
{
  public Patent(string title, string yearOfPublication,
    IEnumerable<string> inventors)
    :this(title, yearOfPublication)
  {
    Inventors.AddRange(inventors);
  }
  private string _Title = title;
  public string Title
  {
    get
    {
      return _Title;
    }
    set
    {
      if (value == null)
      {
        throw new ArgumentNullException("Title");
      }
      _Title = value;
    }
  }
  public string YearOfPublication { get; set; } = yearOfPublication;
  public List<string> Inventors { get; } = new List<string>();
  public string GetFullName()
  {
    return string.Format("{0} ({1})", Title, YearOfPublication);
  }
}

主构造函数语法与属性初始化表达式相结合,简化了 C# 构造函数语法:

  • 无论是只读(请参阅仅定义了 getter 的 Inventors 属性)还是读写(请参阅定义了 setter 和 getter 的 YearOfPublication 属性),自动属性都支持属性初始化,因此,可为属性赋予初始值,以作为属性声明的一部分。 此语法与声明时为字段赋予默认值时使用的语法相匹配(例如,declaration assigned _Title)。
  • 默认情况下,在初始化表达式外部无法访问主构造函数参数。 例如,没有针对类声明的 yearOfPublication 字段。
  • 对只读属性(仅定义了 getter)使用属性初始化表达式时,无法提供验证。 (这是因为,在 IL 底层实现中,主构造函数参数分配给了支持字段。 此外,值得一提的是,如果自动属性只定义了 getter,则支持字段将在 IL 中定义为只读。)
  • 如有指定,则主构造函数将(且必须)始终在构造函数链中最后执行(因此,无法具有 this(...) 初始化表达式)。

另举一例,对于结构声明,其准则表明应是固定不变的。 下面是基于属性的实现(与非常规公共字段方法相对):

struct Pair(string first, string second, string name)
{
  public Pair(string first, string second) : 
    this(first, second, first+"-"+second)
  {
  }
  public string First { get; } = second;
  public string Second { get; } = first;
  public string Name { get; } = name;
  // Possible equality implementation
  // ...
}

请注意,在 Pair 的实现过程中,第二个构造函数会调用主构造函数。 通常,所有结构构造函数必须通过调用 this(...) 初始化表达式直接或间接调用主构造函数。 换句话说,没有必要让所有构造函数都直接调用主构造函数,但会在构造函数链结尾处调用主构造函数。 这是必要的,因为是主构造函数调用基构造函数初始化表达式,并且这样做还可以提供一些保护,避免出现一些常见的初始化错误。 (请注意,在 C# 1.0 中也一样,仍无需调用构造函数即可实例化结构。 例如,这种情况在实例化结构数组后发生。)

无论主构造函数是基于自定义结构还是类数据类型,对基构造函数的调用可以是隐式的(因此,可以调用基类的默认构造函数),也可以是显式的(通过调用特定基类构造函数实现)。 在后一种情况下,对于要调用特定 System.Exception 构造函数的自定义异常,目标构造函数在主构造函数之后指定:

class UsbConnectionException : 
  Exception(string message, Exception innerException,
  HidDeviceInfo hidDeviceInfo) :base(message, innerException)
{
  public HidDeviceInfo HidDeviceInfo { get;  } = hidDeviceInfo;
}

有关主构造函数,要注意的一个细节是,避免重复和可能的不兼容,以及分部类的主构造函数:分部类具有多个部件,只有一个类声明可以定义主构造函数,同样,只有此主构造函数可以指定基构造函数调用。

在此 3 月预览版中实现主构造函数时,需要考虑一个重要的问题:无法向任何主构造函数参数提供验证。 且由于属性初始化表达式仅对自动属性有效,因此无法在属性实现过程中实施验证,而且可能会向无效数据发布实例化分配公开公共属性 setter。 当前顺理成章的解决办法是在验证变得重要时,不使用主构造函数功能。

尽管目前还具有试探性,但已在考虑一个称为“字段参数”的相关功能。 主构造函数参数(如 private string title)中访问修饰符包含的内容会导致将参数捕获到类范围中,作为采用 title 的名称(与参数的名称和大小写匹配)的字段。 因此,title 在 Title 属性或任何其他实例类成员中都可用。 另外,访问修饰符允许指定整个字段语法,包括其他修饰符(如 readonly),甚至是如下属性:

public class Person(
  [field: NonSerialized] private string firstName, string lastName)

请注意,如果没有访问修饰符,则不会允许其他修饰符(包括属性)存在。 访问修饰符指示字段声明将以内联方式与主构造函数一同出现。

(我在撰写本文时所获取的信息不包括字段参数实现,但语言团队保证将在 Microsoft Build 版本中包括进去,因此您应可以在阅读此文时试用字段参数。 此功能相对来说是“最新”的,欢迎立即在 msdn.com/Roslyn 上提供反馈,以便将您的反馈考虑在内,以免流程对更改而言太过久远。

Static Using 语句

另一个 C# 6.0“语法修饰”功能是引入 using static。 您可以使用此功能在调用静态方法时消除对类型的显式引用。 另外,您可以通过 using static 仅对特定类引入扩展方法,而不是在命名空间内引入所有扩展方法。 图 6 提供了 System.Console 上的 using static 的“Hello World”示例。

图 6 使用 Using Static 简化混乱代码

using System;
using System.Console;
public class Program
{
  private static void Main()
  {
    ConsoleColor textColor = ForegroundColor;
    try
    {
      ForegroundColor = ConsoleColor.Red;
      WriteLine("Hello, my name is Inigo Montoya... Who are you?: ");
      ForegroundColor = ConsoleColor.Green;
      string name = ReadLine(); // Respond: No one of consequence
      ForegroundColor = ConsoleColor.Red;
      WriteLine("I must know.");
      ForegroundColor = ConsoleColor.Green;
      WriteLine("Get used to disappointment");
    }
    finally
    {
      ForegroundColor = textColor;
    }
  }
}

本示例中,总共删除了 9 次限定符 Console。 不可否认,此示例是精心设计的,但即便如此,要点也已清晰明了。 通常,静态成员(包括属性)的类型前缀不会起到显著的作用,删除它将会使代码更易写入和读取。

尽管 using static 的第二个(计划的)功能未在 3 月预览版中起作用,但也会对其进行讨论。 此功能支持仅导入特定类型的扩展方法。 请看这个示例,某个实用工具命名空间包括许多具有扩展方法的静态类型。 如果不使用 using static,要么导入该命名空间中的所有扩展方法,要么该命名空间中的扩展方法都不导入。 但通过 using static,您就可以为特定类型(而非更常见的命名空间)查找可用的扩展方法。 因此,您可以通过仅指定 using System.Linq.Enumerable(而非整个 System.Linq 命名空间)来调用 LINQ 标准查询运算符。

遗憾的是,此优势并不是一直可用(至少在 3 月预览版中不是一直可用),这是因为只有静态类型支持 using static,这也是图 6 中没有 using System.ConsoleColor 语句的原因。 考虑到 C# 6.0 当前预览版的性质,是否保留此限制仍有待审核。 您有什么看法吗?

声明表达式

在编写语句的过程中发现需要专门为语句声明一个变量,这种现象并不少见。 请看以下两个示例:

  • 您在编写 int.TryParse 语句代码时,意识到需要为 out 参数声明一个变量,其中将存储分析结果。
  • 您在编写 for 语句时,发现需要缓存集合(如 LINQ 查询结果)以避免多次重新执行查询。 为实现此目的,您中断了编写 for 语句的思考过程,转而声明一个变量。

为了解决这些问题及类似麻烦,C# 6.0 引入了声明表达式。 这意味着您无需将变量声明限制为仅针对语句,它还可以在表达式内使用。 图 7 提供了两个示例。

图 7 声明表达式示例

public string FormatMessage(string attributeName)
{
  string result;
  if(! Enum.TryParse<FileAttributes>(attributeName, 
    out var attributeValue) )
  {
    result = string.Format(
      "'{0}' is not one of the possible {2} option combinations ({1})",
      attributeName, string.Join(",", string[] fileAtrributeNames =
      Enum.GetNames(typeof (FileAttributes))),
      fileAtrributeNames.Length);
  }
  else
  {
    result = string.Format("'{0}' has a corresponding value of {1}",
      attributeName, attributeValue);
  }
  return result;
}

图 7 中的第一个突出显示的内容中,以内联方式声明 attributeValue 变量,并向 Enum.TryParse 发起调用,而不是事先单独声明。 同样,在对 string.Join 发起调用的过程中声明 file­AttributeNames。 这使得在同一语句的稍后部分访问 Length。 (请注意,fileAttributeNames.Length 是 string.Format 调用中的替换参数 {2},即便它较早出现在格式字符串中也是如此,因而可以先声明 fileAttributeNames,然后再对其进行访问。)

声明表达式的适用范围粗略地定义为出现该表达式的语句的适用范围。 在图 7 中,attributeValue 的适用范围即 if-else 语句的适用范围,在条件句的 true 和 false 块中均可访问。 同样,fileAttributeNames 仅适用于 if 语句的前半部分,即与 string.Format 语句调用的适用范围匹配的部分。

编译器将尽可能为声明使用隐式类型化变量 (var),通过初始值设定项(声明赋值)推断数据类型。 但在 out 参数中,调用目标的签名可用于支持隐式类型化变量,即使没有初始值设定项也是如此。 尽管如此,在某些情况下仍无法推断,另外,从可读性角度来讲,这并不是最好的选择。 例如,对于图 7 中的 TryParse,var 起作用的唯一原因是指定了类型参数 (FileAttributes)。 如果未指定,将不会编译 var 声明,相反将需要显式数据类型:

Enum.TryParse(attributeName, out FileAttributes attributeValue)

图 7 的第二个声明表达式示例中,string[] 的显式声明似乎要将数据类型识别为数组(而不是 List<string> 等)。 此原则对于 var 的常规使用而言是标准原则:请考虑在生成的数据类型不明显时避免隐式类型化变量。

图 7 中的声明表达式示例的代码可通过以下方式编写:只需先在语句中声明变量,然后再赋值即可。

异常处理改进

C# 6.0 中包含了两个新的异常处理功能。 第一个是对 async 和 await 语法的改进,第二个是对异常筛选的支持。

C# 5.0 引入了 async 和 await(上下文)关键字,开发人员获得了一种相对简单的方式来编写基于 Task 的异步编程模型 (TAP),其中,编译器承担了一项艰巨而复杂的工作:将 C# 代码转换为一系列基础任务延续。 遗憾的是,团队在该版本中不支持在 catch 和 Finally 块内使用 await。 事实证明,此类调用的需求比最初预期的更为普遍。 因此,C# 5.0 程序员需要应用有效的解决方法(如利用 await 模式)。 C# 6.0 中消除了此缺陷,现在允许 catch 和 finally 块中的 await 调用(在 try 块中已受支持),如图 8 中所示。

图 8 Catch 块中的 Await 调用

try
{
  WebRequest webRequest =
    WebRequest.Create("http://IntelliTect.com");
  WebResponse response =
    await webRequest.GetResponseAsync();
  // ...
}
catch (WebException exception)
{
  await WriteErrorToLog(exception);
}

C# 6.0 中的另一项异常改进(支持异常筛选器)使该语言与其他 .NET 语言(即 Visual Basic .NET 和 F#)保持同步更新。 图 9 显示了此功能的详细信息。

图 9 利用异常筛选器查找要捕捉的异常

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.ComponentModel;
using System.Runtime.InteropServices;
// ...
[TestMethod][ExpectedException(typeof(Win32Exception))]
public void ExceptionFilter_DontCatchAsNativeErrorCodeIsNot42()
{
  try
  {
    throw new Win32Exception(Marshal.GetLastWin32Error());
  }
  catch (Win32Exception exception) 
    if (exception.NativeErrorCode == 0x00042)
  {
    // Only provided for elucidation (not required).
    Assert.Fail("No catch expected.");
  }
}

请注意 catch 表达式后面的另一个 if 表达式。 Catch 块现在不仅确定了 Win32Exception 类型(或由此派生)的异常,而且还确定了另一个条件句(本示例中错误代码的特定值)。 在图 9 中的单元测试中,预期情况是,catch 块将不会捕捉异常(即使异常类型匹配也是如此),相反,异常将被忽略并根据测试方法由 ExpectedException 属性处理。

请注意,与上文所述的 C# 6.0 的一些其他功能(如主构造函数)不同,在 C# 6.0 之前没有编写异常筛选器的等效替代方法。 到目前为止,唯一的方法是捕捉特定类型的所有异常、显式检查异常上下文,之后,如果当前状态不是有效的异常捕捉方案,则重新引发异常。 换句话说,C# 6.0 中的异常筛选提供了 C# 中迄今为止不太可能实现的功能。

其他数字文本格式

尽管 3 月预览版中尚未实现,C# 6.0 将引入一个数字分隔符:下划线字符 (_),将其作为分隔数字文本(十进制、十六进制或二进制)中的数字的一种方式。 可将数字分解为适用于您的方案的任何分组。 例如,可将整数的最大值分组为以千为单位:

int number = 2_147_483_647;

从结果中可更清晰地看到数字(十进制、十六进制或二进制)的量级。

对于新的 C# 6.0 二进制数字文本而言,数字分隔符可能尤其有用。 尽管不是所有的程序中都需要,但使用二进制逻辑或基于标志的枚举时,二进制文本的可用性可以提高可维护性。 例如,请看图 10 中所示的 FileAttribute 枚举。

图 10 为枚举值分配二进制文本

[Serializable][Flags]
[System.Runtime.InteropServices.ComVisible(true)]
public enum FileAttributes
{
  ReadOnly =          0b00_00_00_00_00_00_01, // 0x0001
  Hidden =            0b00_00_00_00_00_00_10, // 0x0002
  System =            0b00_00_00_00_00_01_00, // 0x0004
  Directory =         0b00_00_00_00_00_10_00, // 0x0008
  Archive =           0b00_00_00_00_01_00_00, // 0x0020
  Device =            0b00_00_00_00_10_00_00, // 0x0040
  Normal =            0b00_00_00_01_00_00_00, // 0x0080
  Temporary =         0b00_00_00_10_00_00_00, // 0x0100
  SparseFile =        0b00_00_01_00_00_00_00, // 0x0200
  ReparsePoint =      0b00_00_10_00_00_00_00, // 0x0400
  Compressed =        0b00_01_00_00_00_00_00, // 0x0800
  Offline =           0b00_10_00_00_00_00_00, // 0x1000
  NotContentIndexed = 0b01_00_00_00_00_00_00, // 0x2000
  Encrypted =         0b10_00_00_00_00_00_00  // 0x4000
}

现在您可以使用二进制数字文本更清晰地显示已设置/未设置的标志。 这将取代注释中显示的十六进制计数法或编译时计算转换方法:

Encrypted = 1<<14.

(开发人员若想立即尝试此功能,也可以在 3 月预览版发布后在 Visual Basic .NET 中进行尝试。)

总结

若仅考虑这些语言更改,您将注意到 C# 6.0 中并没有特别的变革或极其重大的改变。 如果您将其与其他重要版本(如 C# 2.0 中的泛型、C# 3.0 中的 LINQ 或 C# 5.0 中的 TAP)进行比较,会发现 C# 6.0 并不只是一个主要版本,更是一个“dot”版本(及时的版本)。 (一个重大新闻是,编译器已经以开源形式发布。)尽管它并未彻底改变您的 C# 编码,但这并不意味着它对消除编码中的一些不足和低效之处没有做出实际贡献,一旦您在日常使用中应用,很快就会将其贡献视为理所当然的功能。 我特别喜欢的功能包括:$ 运算符(字符串索引成员)、主构造函数(不包括字段参数)、using static 和声明表达式。 我希望以上所有功能都能迅速成为我编码过程中的默认选项,在某些情况下,甚至可能添加到编码标准中。

Mark Michaelis 是 IntelliTect 的创始人,担任首席技术架构师和培训师。自 1996 年以来,他就成为 Microsoft C#、Visual Studio Team System (VSTS) 和 Windows SDK 方面的 MVP。他在 2007 年就任 Microsoft 区域总监。另外,他还是多个 Microsoft 软件设计评审团队(包括 C#、连通系统部门和 VSTS)的成员。Michaelis 在开发人员大会上发表讲话,还撰写了许多文章和书籍,目前正在撰写新一版的《Essential C#》(C# 本质论)(Addison-Wesley Professional)。Michaelis 拥有伊利诺伊大学的哲学学士学位,以及伊利诺伊理工学院的计算机科学硕士学位。不在电脑前工作时,他会与家人在一起,或者进行锻炼。

衷心感谢以下 Microsoft 技术专家对本文的审阅:Mads Torgersen