一瞥C# vNext






4.82/5 (34投票s)
对C#下一版本可能的一些语言特性的详细展望。
目录
引言
当前版本的 Visual Studio (2013) 附带了一个以 .NET Framework 4.5.1 形式出现的次要框架更新。缺少了什么?C# 语言的更新!甚至 Roslyn 都没有包含在内,但我认为目前这不是问题,因为它很可能会包含在下一个版本中。我们对 C# 提供的各种可能性不满意吗?我认为这与软件永无止境、总是可以改进的事实无关。
那么 C# 的下一个版本会有什么呢?首先,我们不会看到像 async
/ await
关键字那样改变游戏规则的特性。尽管如此,似乎会有一些非常有用新构造可用。此外,编译器将更好地完成其工作,并在可能的情况下帮助我们。再结合其他即将推出的特性,如 RyuJIT,我们不仅会提高生产力,还会免费获得更高的性能。
目前还有一些工作正在进行中,其中一些特性可能不会进入下一个版本,甚至永远不会。此外,本文对这些特性如何工作非常具有推测性。因此,本文中的所有内容都有一定的失败可能性。一旦下一个版本发布,我将尝试更新本文,以便未来的访问者不会被一些错误信息所迷惑。
背景
C# 已经从一个比原版更糟糕的 Java 克隆演变为一种优雅的语言,拥有大量有用的特性。总而言之,如果目标平台是 Windows,或者希望通过使用 Xamarin 的 Mono 高效地进行跨平台编程,C# 应该是首选语言。
目前 C# 已经发布到第 5 个版本,伴随着强大的并发特性和非常成熟的类型系统。让我们简要回顾一下自 C# 首次发布以来引入的一些各种改进。
- C# v2
- Generics
- 匿名方法
- 局部类型
- 可空类型
- C# v3
- 查询语法
- 扩展方法
- Lambda 表达式
- 类型推断
- 匿名对象
- 自动属性
- C# v4
- 协变/逆变类型参数
- DLR 集成
- 具名和可选参数
- C# v5
- await / async
- 调用者信息
回顾这些信息,让我们来看看 C# 编程语言的下一个版本中可能包含哪些特性。
特性列表
下面列出了 C# 即将发布的版本中可能包含的特性。该特性列表尚未最终确定,因此我在末尾添加了一个概率限定符。高概率意味着我个人认为它很可能会包含在内,而低概率意味着我个人对该特性持怀疑态度。
还应注意,所提供的示例代码无法在当前的 C# 编译器上编译(2013年12月)。即使讨论的特性包含在下一个版本中,语法也可能已更改,或者我犯了错误或笔误。无论如何,请将这些示例视为关于代码在实际中可能如何表现的指南。
主构造函数
主构造函数特性非常巧妙。基本上,此特性旨在解决反复出现的编写如下代码的问题:
class Point
{
int x;
int y;
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
}
哎呀,这只是为了说明“这个类没有空的默认构造函数,并且所有输入参数都分配给字段”就需要写很多代码。因此,C# 的下一个版本可能会在类名中直接包含主构造函数。这可能看起来像这样:
class Point(int x, int y)
{
int x;
int y;
}
在这种情况下,变量 x
和 y
也有可能由编译器自动生成。这将使主构造函数更加有用。
class Point(int x, int y)
{
/* We automatically have fields x and y */
}
这对于这样的小代码片段非常有用。然而,我们可以更好地将其用于构造函数,如以下示例所示。假设我们有以下抽象基类,它没有无参数构造函数。
abstract class Token
{
int code;
public class Token(int code)
{
this.code = code;
}
}
当然,我们可以通过使用主构造函数来简化这种构造,但实际上这并不是我们感兴趣的可能性。我们现在感兴趣的是如何使用主构造函数特性来简化以下类定义。以下代码展示了一个从没有无参数构造函数的基类派生的类。
class CommaToken : Token
{
public CommaToken() : base(0)
{
}
}
这个构造函数表示一些冗余代码,因为我们只是为了选择正确的基构造函数而创建它。这不是很有效,所以对于这样的场景,一种更好的语法将非常受欢迎。让我们用可能的 C# vNext 风格重写它!
class CommaToken() : Token(0)
{
}
用这种语法可以非常优雅地解决问题!因此,主构造函数可以用于简化我们的类和简化基构造函数调用。我们可以定义主构造函数或调用它们。
现在问题是:有什么限制吗?实际上,可能有一个限制。我们仍然可以有其他的构造函数,但是如果放置这些构造函数,我们需要确保主构造函数实际上被调用。让我们看一个例子:
class Point(int x, int y)
{
public Point() : this(0, 0)
{
}
}
另一方面:局部类呢?显然,只允许其中一个部分定义主构造函数,从而使用参数调用基构造函数。所有其他局部定义都不允许这样做。
所有这些对于 struct
类型也应该是有效的,然而,可能会有一些微妙的差异,这与 C# 中结构的独特属性有关。这不改变主构造函数背后的总体思想。此特性似乎很有可能被包含,因为它将减少一些冗余代码。
概率:高
只读自动属性
自动属性真的很有用。我们可以在多种场合使用它们,而且我们应该首先优先于字段使用它们。这背后有原因,但我们不会在这个上下文中讨论它们。
自动属性的问题在于,它们只有在同时表达 getter 和 setter 时才有用。这意味着我们通常会看到很多类似于以下示例的代码:
public int MyProperty
{
get;
set;
}
当然,我们不总是希望将属性设为 public
。我们目前唯一能做的就是改变 set
部分的修饰符。这可能看起来像这样:
public int MyProperty
{
get;
private set;
}
然而,从类的角度来看,我们现在又回到了一个相当荒谬的属性。如果不需要方法特性,为什么还要将这样的 setter 封装在一个方法中呢?当然,这有很好的理由。我们可以轻松地扩展方法体,而无需更改任何其他代码。我们还可以将 setter 再次更改为 public
,或者创建一个 protected
setter。所有这些都可以通过当前的构造轻松实现。然而,对于上述表达式所涵盖的目的,表达式本身似乎增加了太多的开销。
我们想要的是一个(private
)变量来设置和一个(public
)属性来获取。当然,这始终是可能的:
private int value;
public int MyProperty
{
get { return value; }
}
问题在于我们需要编写大量代码来实现一些微不足道的事情,尤其是在我们只想初始化属性的情况下!所以让我们尝试提出一个可能的解决方案。也许我们可以像下面的例子那样直接赋值给属性:
class Point
{
int x;
int y;
public double Dist { get; } = Math.Sqrt(x * x + y * y);
}
自动属性现在有了一种初始化器,但它仍然会创建一个支持字段。当然,这个特性与通过主构造函数初始化的自动字段一起使用时才真正开始有用。让我们看看两者结合:
class Point(int x, int y)
{
public int X { get { return x; } }
public int Y { get { return y; } }
public double Dist { get; } = Math.Sqrt(X * X + Y * Y);
}
如果我们要确保属性只初始化一次,并且之后永远不会改变其值,这种语法似乎很棒。因此,这就像将 readonly
关键字应用于字段一样。
概率:高
静态类型导入
Java 编程语言能够进行静态类型导入。此外,一些熟悉 .NET 的人已经知道 VB 也能够做到这一点。静态类型导入意味着在 static
类中定义的方法(也称为过程和函数)可以被调用,而无需为每次调用指定 static
类的名称。因此,这就像将 static
类视为一个命名空间。从这个意义上说,static
类型导入是使用这个“命名空间”的一种可能性。
那么,可以实现哪些场景呢?让我们首先考虑一个大量使用数学函数的类。通常,需要编写如下代码:
class MyClass
{
/* ... */
public double ComputeNumber(double x)
{
var ae = Math.Exp(-alpha * x);
var cx = Math.Cos(x);
var sx = Math.Sin(x);
var sqrt = Math.Sqrt(ae * sx);
var square = Math.Pow(ae * cx, 2.0);
return Math.PI * square / sqrt;
}
}
静态
类型包含现在允许我们告诉编译器,我们希望像这些函数(本例中为 Exp
、Cos
等)已在此类 MyClass
中定义一样行事。这看起来如何?
using System.Math;
class MyClass
{
/* ... */
public double ComputeNumber(double x)
{
var ae = Exp(-alpha * x);
var cx = Cos(x);
var sx = Sin(x);
var sqrt = Sqrt(ae * sx);
var square = Pow(ae * cx, 2.0);
return PI * square / sqrt;
}
}
这个特性极具争议,可能会导致代码变得奇怪和模糊。然而,最终,应该根据具体情况决定是否真的值得使用这个特性。如果导入的函数名称是 Do
、Create
、Perform
或 Install
,那么混淆很可能成为游戏的一部分。如果导入的是数学函数,那么决定就显而易见。
概率:高
派生属性表达式
前面我们看到,C# vNext 扩展了属性。这很棒,因为属性使得 C# 成为一种非常强大的语言,编写起来也很有趣。属性从一开始就非常有用,但是,随着自动属性的集成,我们摆脱了不使用它们的任何借口。它们无处不在,这绝对是一件好事。
然而,人们希望拥有不反映字段值,而是反映其他字段计算结果的属性。在 UML 中,这种属性称为派生属性(通常用斜杠标记)。现在,没有理由不使用它们。这种属性非常有用,因为它们对于信息隐藏原则至关重要。毕竟,即使像 Count
这样简单的属性,在大多数情况下也可能是派生属性。这意味着实际值在访问属性时计算。
直到今天,我们仍然这样编写派生属性:
public class Point
{
double x;
double y;
public double Dist
{
get { return Math.Sqrt(x * x + y * y); }
}
}
这还能更短吗?嗯,C# 团队显然对此进行了深入思考,在我看来,只有两件事可以缩短:
return
关键字(显然我们总是想返回一些东西)- 大括号(与 Lambda 表达式相同)
事实上,由此产生的语法与 lambda 表达式的语法非常接近。最后,我们只需要为属性分配一个表达式。让我们用 C# vNext 的新语法重写前面的例子。
public class Point
{
double x;
double y;
public double Dist => Math.Sqrt(x * x + y * y);
}
这种语法使用胖箭头运算符将表达式分配给 public
成员 Dist
。这就是我们所说的属性。总而言之,我个人感觉使用 lambda 表达式/胖箭头运算符的倾向性越来越大。考虑到许多人实际上喜欢这种语法,这种趋势是好的。
概率:高
方法表达式
属性表达式非常不错,但这次语言重构过程的教训是什么?如果我们能用属性做到这一点,那么语言中还有哪些部分可能需要这个特性呢?事实上,确实有一个:既然属性只是方法,为什么我们不能以同样的方式修饰方法呢?
嗯,实际上我们应该这样做。目前,在我看来,这也很有可能实现。让我们考虑下面的方法:
public class Point
{
double x;
double y;
public double GetDist()
{
return Math.Sqrt(x * x + y * y);
}
}
为这个特性选择的示例与属性表达式部分给出的示例非常接近,这并非巧合。事实上,这只是一个简单的转换,用来说明这里提出的观点。现在,让我们使用与以前相同的语法,并将其应用于给定的方法。
public class Point
{
double x;
double y;
public double GetDist() => Math.Sqrt(x * x + y * y);
}
毫不奇怪,这确实很相似,并且使用了与以前完全相同的语法。因此,单行方法现在实际上可以像下面的示例一样,构造一个任意类 Rect
:
public class Rect
{
double x;
double y;
double width;
double height;
public double Left => x;
public double Top => y;
public double Right => x + width;
public double Bottom => y + height;
public double ComputeArea() => width * height;
}
和以前一样,这可能很方便,但它并没有短多少。当然,这个特性与允许这些表达式的普遍思想相关联。如果属性没有它们,它们也不会对方法可用。另一方面,即使属性允许这样的表达式,我们也不能确定它们会允许用于方法。实际上有一些反对的理由。
我个人喜欢这个功能,尽管好处微乎其微,但它给语言增添了新鲜感。但每个让 C# 变得更复杂、更高效的功能,也会给新用户带来潜在的障碍。
概率:中
可枚举参数
C# 中的参数是固定的。如果一个方法接受三个参数,我们需要传入三个。当然,我们可以使用默认值,但是我们必须为每个参数指定一个名称。像 C/C++ 中的可变参数是缺失的。
“缺失?!”你说……是的,当然有两个例外。一个是未文档化的特性 __arglist
。但这很难使用。此外,这个特性也不会未文档化,任何人都可以使用它!那么另一个例外是什么呢?当然是 params
关键字。这如何工作?
编译器会识别指定的方法,该方法带有一个 params
参数。此参数必须是最后一个参数。现在编译器会识别所有属于 params
参数的参数,创建一个数组并传递该数组。在方法内部,params
参数的行为就像一个普通数组(因为它就是一个普通数组)。
让我们用一些代码来演示一下:
void Method(int first, params int[] others)
{
/* ... */
}
// Compiler creates Method(1, new int[0]):
Method(1);
// Compiler creates Method(1, new int[1] { 2 }):
Method(1, 2);
// Compiler creates Method(1, new int[2] { 2, 3 }):
Method(1, 2, 3);
// ...
编译器也可能为创建此数组进行一些隐式类型转换。此外,我们可以有更专业的重载,例如,针对 3 个参数或 1 个参数。带 params
参数的方法规范仅在没有其他重载与调用者要求的签名匹配时使用。
到目前为止一切顺利。但归根结底,我们可能并非总是希望访问 params
参数中给出的所有值。此外,代码还有一些小细节,有时很不错,有时却很烦人:
void Method(params object[] p)
{
/* ... */
}
// Compiler uses the call:
Method(new object[] { 1, 2, 3 });
// Compiler creates Method(new object[] { 1, 2, 3 }):
Method(1, 2, 3);
// Compiler creates Method(new object[] { new object[] { 1, 2, 3 } }):
Method(new object[] { 1, 2, 3 }.AsEnumerable());
最后的方法调用说明了问题。为什么我们不能将枚举器作为可变参数传入?C# vNext 现在试图允许这样做。该特性基于与以前完全相同的语法,但是,我们需要将预期的对象从数组更改为可枚举类型。当然,这更加灵活,并且与 LINQ 配合得很好。
这种转换实际上是从 T[]
数组到 IEnumerable
可枚举类型。
void Method(params IEnumerable<int> p)
{
/* ... */
}
// Compiler uses the call:
Method(new int[] { 1, 2, 3 });
// Compiler creates Method(new int[] { 1, 2, 3 }):
Method(1, 2, 3);
// Compiler uses the call:
Method(new int[] { 1, 2, 3 }.AsEnumerable());
这现在是“向后”兼容的,因为每个数组都实现了枚举器。显然,这个特性也很棒,因为它将允许基本上所有可枚举类型作为参数传递,例如 List
实例。
var list = new List<int>();
list.Add(1);
list.Add(2);
/* ... */
// Compiler uses the call:
Method(list);
如果这个特性可用,我们不应该在哪些场景下使用它呢?嗯,有时我们实际上对参数数量感兴趣。在这种情况下,我们将被迫遍历所有元素。当然,可以通过调用 Count()
扩展方法来快捷处理,但是,迭代所需的指令仍然需要执行。因此,我们可以说,如果我们需要固定数量的元素,那么目前现有方法是首选,否则我们绝对应该优先使用可枚举参数。
我个人非常喜欢这个特性,因为它让处理可变数量的参数更加灵活。
概率:中
单子空值检查
这到底是什么?实际上,我知道很多 C# 开发者不知道空值合并运算符,也就是代码中的 ??
。现在又来了一个!另一个处理 null
引用的运算符。null
是引入 bug、拥有比必要更多 if
语句以及在简单程序中引入复杂性的好方法。然而,我们需要未设置引用的表示,否则所有内容都必须回退到某个默认值,而该默认值需要被定义。
现在回到正题:我们仍然(显然)有 null
引用的表示。这也不会消失,至少在 C# 中不会。然而,有时这些结构往往变得非常重复:
int SomeMethodCallWithDefaultValue()
{
var a = SomeMethodCall();
if (a != null)
{
var b = a.SomeProperty;
if (b != null)
{
var c = b.SomeOtherProperty;
if (c != null)
return c.Value;
}
}
return 0;
}
哎呀!这段代码看起来熟悉吗?基本上,我们从某个方法调用中得到结果。这个结果有一些属性,而这些属性又有一些属性,等等。通常,我们只需写 a.b.c.Value
,但是,如果任何属性调用返回 null
引用,那么我们就会遇到麻烦。有些非常有趣的人会写出如下代码:
int SomeMethodCallWithDefaultValue()
{
var a = SomeMethodCall();
try
{
return a.b.c.Value;
}
catch (NullReferenceException)
{
return 0;
}
}
太棒了!这写起来短得多,但代价更高,而且代码真的很愚蠢,因为它不仅会捕获 a
、b
、c
上的 null
引用,还会捕获属性调用内部的 null
引用。因此,任何“真正”的 null
异常也会被捕获,而我们却不会注意到这是一个真正的 bug 发生了。
结论是,除了编写冗长的代码,我们别无选择。或者我们使用 null
合并运算符:
int SomeMethodCallWithDefaultValue()
{
var a = SomeMethodCall();
return (((a ?? new A()).b ?? new B()).c ?? new C()).Value;
}
这行得通,但效率不高。这里的主要问题是,我们为每个缺失的引用都分配了一个新对象。这个对象只需要很短的时间。如果我们现在非常频繁地调用 SomeMethodCallWithDefaultValue
方法,我们可能会遇到性能问题,因为 GC 需要运行得过于频繁。
当然,有一个解决方案是提供“默认”对象。例如 EventArgs.Empty
。基本上,这只是 new EventArgs()
,但是预先分配在 static
只读变量中。我们的例子因此会转换为:
int SomeMethodCallWithDefaultValue()
{
var a = SomeMethodCall();
return (((a ?? A.Empty).b ?? B.Empty).c ?? C.Empty).Value;
}
现在问题是:当这些类可以自动返回默认实例而不是 null
引用时,我们为什么要这样做呢?我们可能永远不会知道……回到最初的问题:我们如何才能实现与前面的代码相同的功能,但无需创建新实例或使用示例实例?答案当然由本节的标题给出:单子 null
检查!
C# vNext 可能会引入一个新的点运算符:?.
。显然,这可以创建一个链,一旦检测到 null
引用就会中断。在这种情况下,我们显然需要一个默认值。否则,我们将返回链的结果。
在 C# 代码中,这条链会是什么样子?让我们从我们之前的例子来看:
int SomeMethodCallWithDefaultValue()
{
var a = SomeMethodCall();
return a?.b?.c?.Value ?? 0;
}
看起来有点像三元条件运算符。这似乎不是很复杂,但在隐藏复杂性方面做得很好。显然,null
值是必需的(即,nullable
结构或一般的类)才能使其工作。否则,使用 null
合并运算符就没有多大意义了。否则,如果我们中断链,但不提供 null
合并运算符,即默认值,结果会是什么?
尽管这个特性看起来再次非常有用,但有一些非常重要的问题需要讨论。为什么不应该包含值类型?如果包含值类型,它们是否应该被转换为可空类型?当然,链不能在值类型上中断,因为它们永远不会是 null
,然而,如果最后一个(即返回的)值是值类型,那么我们不能使用 null
合并运算符,如前所述。
然而,对于结构体仍然需要一些处理。现在让我们看看带有一个可能的解决方案的代码:
int SomeMethodCallWithDefaultValue()
{
var a = SomeMethodCall();
var c = a?.b?.c;
return c != null ? c.Value : 0;
}
这个解决方案通常避免了值类型。因此,我们不能像之前那样使用它。然而,通过使用单子 null
检查,我们可以降低方法的复杂性。此外,整个事情看起来更具可读性。
让我们看看另一个可能的解决方案:
int? SomeMethodCallWithDefaultValue()
{
var a = SomeMethodCall();
return a?.b?.c?.Value ?? 0;
}
显然,这引入了隐式转换。那么哪种解决方案更好呢?两者都有其缺点,但也有其优点。由于需要做出决定,因此必须彻底考虑所有选项。我认为拥有这样的功能会非常好,但我能想到的问题必须得到回答。让我们看看这样的功能是否能进入下一个版本,如果能,C# 团队是如何解决的。
概率:高
构造函数类型推断
C# 中的泛型方法非常棒。它们为什么如此棒?嗯,这些方法具有所谓的类型推断。事实上,如果我们看下面的代码,我们就会明白这是什么意思:
void Read<T>(T obj)
{
/* ... */
}
//...
string a = "Hi";
int b = 5;
Read(a);//T is resolved as string
Read(b);//T is resolved as int
Read<double>(5.0);//Explicit, T is double
所以只需调用该方法,编译器就能够检测到类型参数。这仅适用于所有类型参数直接或间接用于参数的情况。以下方法不能与类型推断一起使用:
T Write<T>()
{
/* ... */
}
编译器无法通过查看方法调用来解析 T
(在这种情况下,它总是看起来像 Write()
)。当然,有人可能会争辩说,如果考虑像 int a = Write()
这样的表达式,理论上推断这种类型是可能的。然而,C# 编译器目前不支持这一点。
这种带有方法的类型推断甚至更进一步。让我们考虑以下一组方法:
void DoStuff()
{ }
void DoStuff<T>(T obj)
{ }
void DoStuff(int a)
{ }
显然,我们可以有泛型方法的重载。如果我们不指定参数,则使用第一个方法。在单个参数的情况下,选择取决于参数的类型。在整数的情况下,将使用最后一个方法。在任何其他情况下,我们将使用泛型方法。
实际上,我们也可以在类中遇到类似的情况。假设我们有以下类型的类:
class MyClass
{ }
class MyClass<T>
{ }
这与方法的情况非常相似。如果我们从构造函数的角度考虑,它甚至会更相似。构造函数是我们免费获得的方法。一旦分配了新对象,它就会自动调用。此外,构造函数控制是否可以构造对象以及实例化对象需要哪些参数。要触发构造函数的调用,我们需要使用 new
关键字实例化对象。
对于上面的两个类,可以通过以下指令实现:
var nongeneric = new MyClass();
var generic = new MyClass<int>();
现在,已构建的情况类似于 T Write
。在这里,如果不显式命名类型参数,则无法从指令中推断类型参数。但是下面的情况呢?
class MyClass
{ }
class MyClass<T>
{
MyClass(T obj)
{ }
}
在这里,我们将泛型版本的隐式给定标准构造函数替换为一个接受一个参数的构造函数。该参数与类的泛型类型参数具有相同的类型。
现在我们可以问的问题是:以下调用是否可能?
var nongeneric = new MyClass();
int parameter = 5;
var generic = new MyClass(parameter);
显然答案是否定的,至少目前是这样。但为什么呢?这背后的原因是各种构造函数之间可能存在的歧义。让我们再次更改我们的示例:
class MyClass
{
MyClass(int p)
{ }
}
class MyClass<T>
{
MyClass(T obj)
{ }
}
在这种情况下,我们有两个可能的问题:
- 如果传入整数参数,我们不能确定地选择泛型版本。显式方法以前具有更高的优先级,为了保持一致性,在这种情况下我们也需要优先选择显式构造函数。
- 在其他情况下,我们不知道参数是实际被错误传递,还是泛型版本真的应该能够处理这种情况。
正如前面提到的,旧的 C# 版本对此问题有一个非常优雅的解决方案:通过不允许构造函数类型推断来避免问题。C# vNext 最终希望包含构造函数类型推断。那么我们的两个问题呢?
- 与方法一样,显式构造函数将优先。这意味着非泛型优于泛型。在这种情况下,我们仍然需要显式表达类型参数。
- 如果泛型构造函数匹配签名,则将采用泛型构造函数。我们有责任通过不使用
var
或查看编译器推断的类型来避免错误。
总而言之,我认为这个功能非常有用。这意味着,例如,我们可以这样写:
var pair = new KeyValuePair<int, string>(0, "zero");
……我们可以这样写相同的语句:
var pair = new KeyValuePair(0, "zero");
此功能很可能被引入,因为它已经缺失了一段时间。另一方面,我们不应该忘记它缺失的原因。因此,在使用构造函数类型推断时要小心是必须的。
概率:中
Out 参数推断
C# 引入了两种通过引用传递值的方式。一种是使用 ref
参数,另一种是使用 out
关键字。我个人喜欢为几乎相同的事情提供两个关键字(尽管编译器对 out
参数施加了一些确实有意义的限制)。毕竟在 C 语言中,需要对引用/返回参数名称引入某种约定。否则,其他程序员就无法知道该参数是如何被例程使用的。
有些人希望有一种在调用方法时声明变量的方法,该方法具有 out
参数。这看起来有点奇怪,但在某些情况下可能很有用:
//Calling bool int.TryParse(string input, out int value);
if (int.TryParse(someInput, out var a))
{
// ...
}
让我们将其与我们今天会写的方式进行比较:
var a = 0;//alternatively: int a;
if (int.TryParse(someInput, out a))
{
// ...
}
那么它将如何使用呢?嗯,有一些未决问题。一个肯定是,这是否只是一个临时容器,在初始化之后无法访问。另一个是,变量存在于哪个作用域中?显然,if
语句主体使用的作用域不是父级,因为需要方法调用才能进入该作用域。另一方面,父作用域似乎也不对,毕竟我们可能遇到这个方法永远不会被调用的情况。
这样的场景很容易构建:
if (someCondition)
{
// ...
}
else if (int.TryParse(someInput, out var a))
{
// ...
}
在这种情况下,如果条件不满足,则调用该方法。现在的问题是:在整个条件块之后,变量 a
是否可用?
这些问题似乎与实际应用相去甚远,但事实上并非如此。这种语法确实存在一些悬而未决的问题,这就是为什么我怀疑我们不会在下一个版本中看到它,除非所有这些问题都可以在没有歧义的情况下得到解决。
概率:高
二进制字面量和分隔符
C# 支持各种字面量。对于数字字面量,C# 引入了后缀表示法,例如,使用 f
表示单精度浮点数,或使用 m
表示固定精度十进制数。此外,我们可以使用各种数字表示法。
当然,对我们人类来说最自然的表示法是十进制。数字 1234
的值实际上是 1234
。然而,C# 也允许我们使用十六进制系统定义数字。这是一个基数为 16 的系统,与我们的基数为 10 的系统不同。这意味着每个数字不是乘以 10^p
的因子,而是乘以 16^p
,其中 p
是从数字右侧开始的 0 基位置。在这种表示法中,数字 1234
的值实际上是 4660
。我们可以通过评估表达式 1*16^3 + 2*16^2 + 3*16^1 + 4*16^0
来计算这个值。
十六进制系统在某些情况下非常有用,但有时它只是二进制系统的一种更压缩的版本。我们可以直接看到 8 个二进制位可以用 2 个十六进制位表示。这是为什么呢?一方面,我们有 2^8
种可能性,另一方面有 16^2
种可能性。让我们稍微评估一下左侧并进行转换:我们得到 2^8=2^(2*4)=4^4
。对于右侧,我们可以做同样的事情。最后,我们得到 16^2=4^(2*2)=4^4
。另一方面,可以通过首先意识到 16=2^4
来捷径。
但不要太快:有时,我们不想用接近二进制的表达式来表示数字,而是直接用这种基数表示。这就是我们需要开始思考的地方。C# 现在试图通过引入二进制字面量来帮助我们。让我们从我们目前拥有的开始:
var num1 = 1234; //1234
var num2 = 0x1234; //4660
现在可能出现什么?答案在这里:
var num3 = 0b1010; //10
当然,二进制数字会很快变得非常长。这就是为什么引入了一个漂亮的分隔符:
var num4 = 0b1100_1010; //202
下划线可以有多少就多少。下划线也可以连接起来。最棒的是:下划线也适用于普通数字和十六进制字面量。
var num5 = 1_234_567_890; //123456789
var num6 = 0xFF_FA_88_BC; //4294609084
var num7 = 0b10_01__01_10; //150
下划线唯一的限制是,数字当然不能以下划线开头。
二进制字面量将使枚举和位向量更容易理解和处理。它更接近我们创建这些构造时所想的。
概率:高
兴趣点
与所有语言特性一样,人们对其中一些的讨论会多于另一些。同样,随着争议因素的增加,有些人会比其他人更喜欢或不喜欢某些特性。然而,应该注意的是,每个语言特性都只是另一种语法糖。下一个版本完全向后兼容,因此只会让我们提高生产力。
就个人而言,我最期待主构造函数和单子空值检查。额外的泛型类型推断也相当不错。可枚举参数早该有了,能给我们带来更大的灵活性。我也非常喜欢二进制字面量,因为这是一件相对容易实现,但在大多数其他语言中却不具备的功能。属性功能增强了可能性,很不错,但我不认为它是绝对必需的。
参考文献
关于 C# vNext 的唯一官方信息由 Mads Torgersen 在 NDC 2013 的演讲中提供。此后,许多与会者发布了他在演讲中展示的一些特性。这些是我撰写本文时使用的参考资料:
- Mads Torgersen 在 NDC 2013 的演讲
- http://damieng.com/blog/2013/12/09/probable-c-6-0-features-illustrated
- http://adamralph.com/2013/12/06/ndc-diary-day-3/
- 官方 codeplex 主页[^]
由于 Roslyn 现在是开源的,C# 的下一个版本也公开发布并开放讨论。
图片取自 http://www.brothers-brick.com/2006/06/09/star-trek-the-next-generation-minifigs/[^]。
历史
- v1.0.0 | 首次发布 | 2013年12月20日
- v1.0.1 | 添加了文章图片信息 | 2013年12月21日
- v1.1.1 | 添加了关于二进制字面量的章节 | 2014年4月4日v1.1.2 | 更新了概率 | 2014年6月12日