借助Visual Studio 2017,Microsoft增加了C#的发布节奏。
在历史上与新的Visual Studio版本一致的主要版本之间,他们开始发布次要版本作为选定的Visual Studio 2017更新的一部分。次要版本将包括较小的新功能,不需要更改公共语言运行时(CLR)。
本文已在C#7.1,7.2和7.3中更新了新功能,网址为www.dotnetcurry.com/csharp/1437/csharp-7-1-7-2-7-3-new-features
较大的功能仍将仅与主要版本一起发布。
您是否跟上新的开发人员技术?通过我们的免费开发人员杂志推进您的IT职业生涯,涵盖C#,模式,.NET核心,MVC,Azure,Angular,React等。免费订阅DotNetCurry(DNC)杂志并下载所有以前,当前和即将推出的版本。
C#7.1 - 什么是新的
C#7.1于2017年8月发布,作为Visual Studio 2017 15.3更新的一部分。与过去的新语言版本不同,这次更新Visual Studio后不会自动启用新功能; 既不在现有项目中,也不在创建新项目时。
如果我们尝试使用新的语言功能,则生成的构建错误将建议升级正在使用的语言版本。
图1:新语言功能的构建错误
可以在项目属性中更改语言版本。
在“ 构建”选项卡上有一个“ 高级”按钮,该按钮将打开一个对话框,其中包含用于选择语言版本的下拉列表。默认情况下,选择最新的主要版本,目前为7.0。
编辑说明:如果您不熟悉 C#7,请阅读我们的教程,网址为www.dotnetcurry.com/csharp/1286/csharp-7-new-expected-features
我们可以选择特定版本(7.1以获取新功能)或最新的次要版本,它将始终自动使用当前可用的最新版本。
图2:更改语言版本
默认情况下不选择后一个选项。
这样开发团队就可以控制他们采用新的次要语言版本的方式。如果新的语言功能自动可用,这将迫使团队中的每个人在第一次使用单个新功能时立即更新Visual Studio,否则项目的代码将无法编译。
所选语言版本保存在项目文件中,不仅是项目特定的,还包括特定于配置的。
因此,改变项目属性的语言版本时,请确保每个配置做到这一点,甚至更好:设置配置上构建选项卡,所有配置应用变化之前。
否则,您可能最终只更改Debug配置的语言版本,导致版本配置失败。
图3:“构建”选项卡上的配置选择
对于某些语言功能,还有一个可用的代码修复程序,它将语言版本更改为7.1或最新的次要版本。它将自动为所有配置执行此操作。
图4:用于更改语言版本的代码修复
C#7.1中引入了四种新的语言功能
异步主要
对于C#7.0已经考虑了对异步主函数的支持,但是推迟到C#7.1。该功能简化了使用异步方法异步和等待从控制台应用程序的语法。在C#7.1之前,作为程序入口点的Main方法支持以下签名:
public static void Main();
public static int Main();
public static void Main( string [] args);
public static int Main( string [] args);
|
由于异步方法只能在从其他异步方法内部调用时等待,因此需要额外的样板代码才能使其工作:
static void Main( string [] args)
{ MainAsync(args).GetAwaiter().GetResult();
} static async Task MainAsync( string [] args)
{ // asynchronous code
} |
使用C#7.1,Main方法支持异步代码的其他签名:
public static Task Main();
public static Task< int > Main();
public static Task Main( string [] args);
public static Task< int > Main( string [] args);
|
使用其中一个新签名时,可以直接在Main方法中等待异步方法。编译器将生成必要的样板代码以使其工作。
默认文字表达式
默认值表达式可用于返回给定类型的默认值:
int numeric = default ( int ); // = 0
Object reference = default (Object); // = null
DateTime value = default (DateTime); // = new DateTime()
|
当我们事先不知道给定类型的默认值是什么时,它们与泛型类型结合使用时特别有用:
bool IsDefault<T>(T value)
{ T defaultValue = default (T);
if (defaultValue != null )
{
return defaultValue.Equals(value);
}
else
{
return value == null ;
}
} |
C#7.1增加了对默认文字表达式的支持,只要可以从上下文中推断出类型,就可以使用它来代替默认值表达式:
int numeric = default ;
Object reference = default ;
DateTime value = default ;
T defaultValue = default ;
|
新的默认文字表达式不仅在变量赋值中有用,它还可以在其他情况下使用:
在回报声明中,
作为可选参数的默认值,
作为调用方法时的参数值。
文字表达式语法等同于值表达式语法,但更简洁,特别是对于长类型名称。
推断的元组元素名称
元组最初是在C#7.0中引入的。C#7.1仅对其行为进行了微小改进。在C#中创建元组时,必须明确给出元素名称,或者只能通过默认名称Item1,Item2等访问元素:
var coords1 = (x: x, y: y); var x1 = coords1.x; var coords2 = (x, y); var x2 = coords2.Item1; // coords2.x didn't compile
|
在C#7.1中,可以从用于构造元组的变量名称推断出元组名称。因此,以下代码现在编译并按预期工作:
var coords2 = (x, y); var x2 = coords2.x; |
通用模式匹配
C#7.0中最重要的新功能之一是使用is关键字和switch语句进行模式匹配。类型模式允许我们根据值类型进行分支:
void Attack(IWeapon weapon, IEnemy enemy)
{ switch (weapon)
{
case Sword sword:
// process sword attack
break ;
case Bow bow:
// process bow attack
break ;
}
} |
但是,这对于一般类型的值不起作用。例如,以下代码未在C#7.0中编译:
void Attack<T>(T weapon, IEnemy enemy) where T : IWeapon
{ switch (weapon)
{
case Sword sword:
// process sword attack
break ;
case Bow bow:
// process bow attack
break ;
}
} |
C#7.1扩展了类型模式以支持泛型类型,使上面的代码有效。
C#7.2 - 新的预期功能
语言开发并没有随着C#7.1的发布而停止。该团队已经在研究下一个次要版本 - 7.2。发布日期尚未公布,虽然所有规范和讨论都是公开的,但新功能无法轻易尝试。
随着发布的临近,我们可以预期支持新功能的更新编译器将包含在Visual Studio 2017 Preview中,可以安全地与当前的Visual Studio 2017版本一起安装。
目前为C#7.2计划了几种新的语言功能,但它们仍有可能发生变化。其中一些可能会推迟到更高版本,并且可能会添加新功能。
基本说明符后的数字分隔符
在C#7.0中,允许在数字文字中使用分隔符以提高可读性:
var dec = 1_000_000; var hex = 0xff_ff_ff; var bin = 0b0000_1111; |
另外,C#7.2计划在基本说明符之后允许分隔符:
var hex = 0x_ff_ff_ff; var bin = 0b_0000_1111; |
非尾随命名参数
命名参数在版本4中被添加到C#中。它们主要是允许可选参数的工具:调用方法时可以跳过某些参数,但是对于它后面的所有参数,必须对参数进行命名,以便编译器可以匹配他们:
void WriteText( string text, bool bold = false , bool centered = false )
{ // method implementation
} // method call WriteText( "Hello world" , centered: true );
|
如果参数不是可选的,仍然可以命名参数以提高代码可读性,如果你不记得它是什么,你甚至可以改变参数的顺序:
WriteText( "Hello world" , true , true ); // difficult to understand
WriteText( "Hello world" , bold: true , centered: true ); // better
WriteText( "Hello world" , centered: true , bold: true ); // different order
|
但是,C#还不允许位置参数在同一方法调用中遵循命名参数:
WriteText( "Hello world" , bold: true , true ); // not allowed
|
根据目前的计划,这将成为C#7.2中有效的方法调用。即使它们遵循命名参数,也将允许位置参数,只要所有命名参数仍处于正确位置且名称仅用于代码说明目的。
私人保护
公共语言运行时(CLR)支持类成员可访问级别,该级别在C#语言中没有等效,因此无法使用:可以从子类访问protectedAndInternal成员,但前提是子类与基类在同一个程序集中宣布该成员。
在C#中,基类开发人员当前必须在两个完全不符合此行为的访问修饰符之间进行选择:
protected将使该成员仅对子类可见,但它们可以在任何程序集中。不必限制它们必须放在同一个组件中。
internal将限制成员对同一程序集的可见性,但该程序集中的所有类都将能够访问它,而不仅仅是声明它的基类的子类。
对于C#7.2,计划一个新的访问修饰符:private protected将匹配protectedAndInternal可访问性级别 - 成员只能在同一个程序集中对子类可见。这对于图书馆开发人员来说非常有用,他们不需要在图书馆外公开受保护成员和让图书馆内所有课程都可以使用内部成员之间做出选择。
条件参考运算符
在C#7.0中,引入了对引用返回值和局部变量的支持。您可以从我之前在Dot Net Curry(DNC)杂志上关于C#7.0的文章中了解更多信息。
但是,目前无法通过引用不同的表达式来有条件地绑定变量,类似于三元组或条件运算符在按值绑定时所执行的操作:
var max = a > b ? a : b; |
由于引用的变量绑定不能反弹到不同的表达式,因此无法使用if语句解决此限制:
ref var max = ref b; // requires initialization
if (a > b)
{ r = ref a; // not allowed in C# 7.1
} |
在某些情况下,以下方法可以替代:
ref T BindConditionally<T>( bool condition, ref T trueExpression, ref T falseExpression)
{ if (condition)
{
return ref trueExpression;
}
else
{
return ref falseExpression;
}
} // method call ref var max = ref BindConditionally(a > b, ref a, ref b);
|
但是,如果在调用方法时无法评估其中一个参数,它将失败:
ref var firstItem = ref BindConditionally(emptyArray.Length > 0, ref emptyArray[0], ref nonEmptyArray[0]);
|
这将抛出IndexOutOfRangeException,因为仍将评估emptyArray [0]。
使用为C#7.2规划的条件引用运算符,可以实现所描述的行为。就像现有的条件运算符一样,只会评估所选的替代选项:
ref var firstItem = ref (emptyArray.Length > 0 ? ref emptyArray[0] : ref nonEmptyArray[0]);
|
参考本地重新分配
对于C#7.2计划的引用绑定的局部变量和参数的另一个扩展 - 将它们重新绑定到不同表达式的能力。通过此更改,上一节中缺少条件引用运算符的解决方法也可以正常工作:
ref var max = ref b;
if (a > b)
{ r = ref a;
} |
只读参考
在性能敏感的应用程序中,结构通常通过引用传递给被调用函数,而不是因为它应该能够修改值,而是避免复制值。目前无法在C#中表达,因此只能在文档或代码注释中解释,这纯粹是非正式的,没有保证。
为了解决这个问题,C#7.2计划包括对通过引用传递的只读参数的支持:
static Vector3 Normalize( ref readonly Vector3 value)
{ // returns a new unit vector from the specified vector
// signature ensures that input vector cannot be modified
} |
语法尚未最终确定。相反的裁判只读,在可以使用。甚至可以允许两种语法。
Blittable类型
Common Language Runtime中有一个非托管或blittable类型的概念,它在托管和非托管内存中具有相同的表示形式。
这允许它们在托管代码和非托管代码之间传递而无需转换,从而使它们更具性能,因此在互操作性方案中非常重要。
在C#中,如果结构只由blittable基本类型(数字类型和指针)和其他blittable结构组成,则它们当前是隐式blittable。由于没有办法明确地将它们标记为blittable,因此没有编译时间保护来防止这些结构的无意更改,这会使它们变得不易受影响。
这样的变化可以产生非常大的影响,因为任何其他结构(包括变得不闪烁的结构)也将变得不易受影响。这可以打破消费者,而开发人员不会意识到这一点。
C#7.2有一个计划为blittable结构添加一个显式声明:
blittable struct Point
{ public int X;
public int Y;
} |
将结构声明为blittable的要求将保持不变。但是,这样的结构不会自动被认为是blittable。为了使它blittable,它必须用blittable关键字明确标记。
通过这种更改,编译器可以在对结构进行更改时使开发人员发出非blittable的同时警告开发人员,同时仍将其声明为blittable。它还允许blittable关键字用作泛型类型的约束,允许执行泛型辅助函数,这需要它们的参数是blittable。
C#8 - 即将发生的事情
在开发下一个次要语言版本的同时,还在下一个主要版本上开展工作。所有当前计划的功能都具有很大的范围和影响。它们仍处于早期原型阶段,可能远离发布阶段。
可空参考类型
在C#7.0开发的早期阶段已经考虑过这个功能,但是推迟到下一个主要版本。它的目标是帮助开发人员避免未处理的NullReferenceExceptions。
核心思想是允许变量类型定义包含信息,无论它们是否可以为它们分配空值:
IWeapon? canBeNull; IWeapon cantBeNull; |
将空值或潜在空值分配给不可为空的变量将导致编译器警告(开发人员可以将构建配置为在发生此类警告时失败,以确保额外安全):
canBeNull = null ; // no warning
cantBeNull = null ; // warning
cantBeNull = canBeNull; // warning
|
这种改变的问题在于它破坏了现有代码:假设改变之前的所有变量都是不可为空的。为了解决这个问题,可以在项目级别以及引用的程序集级别禁用对零安全性的静态分析。
当开发人员准备好处理结果警告时,开发人员可以选择进行可空性检查。尽管如此,这仍然符合她/他自己的最佳利益,因为警告可能会揭示他的代码中可能存在的错误。
递归模式
第一个模式匹配功能已添加到版本7.0中的C#。有计划进一步扩展C#8.0的支持。
递归模式是计划添加之一。它们将允许部分数据与子模式匹配。
该提案列出了符号表达式简化器作为可以使用此功能实现的示例。它需要支持递归类型作为表达表达式的方法:
abstract class Expr;
class X() : Expr;
class Const( double Value) : Expr;
class Add(Expr Left, Expr Right) : Expr;
class Mult(Expr Left, Expr Right) : Expr;
class Neg(Expr Value) : Expr;
|
然后,简化可以作为递归函数实现,严重依赖于模式匹配:
Expr Simplify(Expr e) { switch (e) {
case Mult(Const(0), _): return Const(0);
case Mult(_, Const(0)): return Const(0);
case Mult(Const(1), var x): return Simplify(x);
case Mult(var x, Const(1)): return Simplify(x);
case Mult(Const(var l), Const(var r)): return Const(l*r);
case Add(Const(0), var x): return Simplify(x);
case Add(var x, Const(0)): return Simplify(x);
case Add(Const(var l), Const(var r)): return Const(l+r);
case Neg(Const(var k)): return Const(-k);
default : return e;
}
} |
默认接口方法
目前不允许C#中的接口包含方法实现。它们仅限于方法声明:
interface ISample
{ void M1(); // allowed
void M2() => Console.WriteLine( "ISample.M2" ); // not allowed
} |
要实现类似的功能,可以使用抽象类:
abstract class SampleBase
{ public abstract void M1();
public void M2() => Console.WriteLine( "SampleBase.M2" );
} |
尽管如此,仍计划在C#8.0中添加对默认接口方法的支持,即使用上面第一个示例中建议的语法实现方法。这将允许抽象类不支持的场景。
库作者可以使用默认接口方法实现扩展现有接口,而不是使用方法声明。
这样做的好处是不会破坏现有的类,这些类实现了旧版本的接口。如果他们没有实现新方法,他们仍然可以使用默认的接口方法实现。当他们想要改变那种行为时,他们可以覆盖它,但是因为接口被扩展所以不需要改变代码。
由于不允许多重继承,因此类只能从单个基本抽象类派生。
与该限制相反,类可以实现多个接口。如果这些接口实现了默认的接口方法,这有效地允许类从多个不同的接口组成行为 - 这个概念称为trait,并且已经在许多编程语言中可用。
与多重继承不同,当在多个接口中定义具有相同名称的方法时,它避免了所谓的菱形问题。为此,C#8.0将要求每个类和接口对每个继承的成员具有最特定的覆盖。
当具有相同名称的成员从多个接口继承时,当其接口从另一个接口派生时,一个覆盖比另一个覆盖更具体。当任何接口都不直接或间接地从其他接口继承时,开发人员将需要指定他想要使用的覆盖或编写自己的覆盖。
通过这样做,他将明确解决歧义。
结论:
C#编译器实现了Roslyn的承诺:由于全新的代码库,可以更快地引入新功能。
与此同时,新功能并未强加给那些喜欢对他们使用的语言版本进行更严格控制的大型团队。他们可以评估新功能,并在他们想要采用它们时按照自己的进度决定。
根据开源模型,即使未来版本的C#语言中即将出现的功能状态也是公开的,可供所有人探索甚至贡献他们的意见。重要的是要记住,这些功能仍在进行中,因此它们可以在没有警告的情况下进行更改,甚至可以推迟到更高版本。