Gotocph mads torgersen cover

为什么要用 C# 来作为您的首选编程语言

因为您可以用,并且也是您的最佳选择!之所以可用,是因为 C# 能够很好地在 Mac、Linux、Android 和 iOS 上运行(对了,还有 Windows);它可以在您最喜爱的编辑器上运行;它在一个稳定的企业级平台上经过了充分的时间验证;最为重要的是:它是完全开源的!之所以是您的最佳选择,是因为 C# 是编程语言创新方面的领导者,是原生跨平台移动应用程序的最佳选择,并且还有很多的优点超乎您的想象。在本次 GOTO Copenhagen 2016 大会讲演上,Mads Torgersen 邀请您一起来探索 C# 的核心,探究为什么它仍然散发着活力,并探寻未来 C# 的发展趋势。

概述

我是 Mads Torgersen,就职于微软的 C# 部门。我现在年纪大了,因此我随身都穿着这件 T恤,上面印着我正在负责的项目和语言名称,以防我忘掉它们。这里我想谈一谈 C#,为什么我要推荐用它来作为大家的首选编程语言呢(即使到目前为止您还没有接触过 C#)。

Stack Overflow - 最受欢迎和喜爱的技术

Stack Overflow 每年都会进行一次调查,询问很多开发者们都关心的问题(当然,在很多方面这些问题都是很具有倾向性,是很不科学的)。您必须在 Stack Overflow 上才能参与。

C# 是一门被广泛使用的编程语言(排行第四,排行前三当中有一门实际上并不属于编程语言——我说的不是 JavaScript,我说的是 SQL)。可以看出,C# 是一门主流语言。

他们同样还问开发者们:是否还想继续使用目前正在用的语言,并让人们投票出他们最喜爱的技术。C# 同样也在这个列表当中。这说明人们都很喜欢 C# 这门语言。此外还有其他人们也喜欢的语言,但是您还可以注意到,这些语言中的大部门要么就是受众较少,要么就是非常专业化,很多都是某种狂热信仰的一部分了。在这两个列表当中,只有少数才是用途广泛、受人们高度喜爱的。很高兴能看到 C# 位于这个列表的三大最受欢迎的技术之一,其中两个是编程语言,并且* Python 也在这里面*。

我们不断思考我们的所作所为,怎样才可能是正确的呢,怎样才能让我们在多年以后仍然喜欢它。似乎并不是所有人都用过 C#,因为很多人所在的公司已经有 10 年多的历史了,里面存在了很多的遗留代码。目前 C# 仍然保持着活力,我们希望它能将这份活力保持下去。我们同样也有各式各样的想法,而这驱动了 C# 的演进。

我们非常渴望去演进 C#。如果您看过现代语言的演变进程的话(从少到多),就会明白我们积极保持语言现代化的目的所在了。作为参与编程语言演变的一份子,我们有些时候是推动者,有些时候是跟随者,无论如何,我们都试图让 C# 成为程序员们如今的绝佳选择之一。我们不应该搞所谓的「限定」,只局限于某几个平台,因为过去十年当中就有人这么做了,结果可想而知。

我还想提一提 F#,因为这相当于是我们的姊妹语言,它非常受欢迎,因为它很轻巧、也很强大。F# 是一门功能强大的语言,我们在与 F# 团队的合作当中获益良多,并且它也给我们提供了很多设计灵感。

时代在改变 - 为什么要选择 C#

在越来越多的场景当中,您都可以使用 C# 来进行编程。我们正在努力地做出一种改变。C# 在 Windows 当中是一种很重要的主要编程语言,但同时,我们在其他平台上仍然还非常稚嫩。至少大多数平台是这样。现在 C# 已经是所有平台上可选的编程语言之一,这非常鼓舞人心,然而我们同时也有些顽固,此外这些平台上也出现了各式各样新颖的语言。这使得我们迫切地希望其他平台上也能够使用我们的语言。

我们已经很多次对我们的语言进行了演进。实现 C# 底层的编译器和 IDE 技术(名为 Roslyn 项目)为 C# 的编程启用了独一无二的场景。其中一个好处是,我们将 C# 的核心从 Windows 和 Visual Studio 当中剥离了出来,这意味着 C# 能够很容易地在其他 IDE 当中使用。您可以用自己喜爱的 IDE 或者编辑器来编写 C# 代码。

我们已经将 C# 从完全的专有技术转变为了完全开放源代码的技术了。这意味着每个人都能给 C# 贡献代码了,当然也已经很多人参与到这个项目当中来了。我们现在正在同社区展开交流,现在 C# 的演进非常迅速。因为现在这更像是一个协同项目了,而不是「微软说怎样就怎样了」。这非常让人兴奋。现在语言的变革已经不再是三年才一代了。「这是我们努力的成果,希望您能喜欢它」,我们现在每天都在与社区讨论未来的方向问题。我们随时随地都能够在网上、Github 上得到反馈。因此,我们语言设计的质量也越来越高。

让我们从 C# 的各个项目开始一一介绍。

无处不 C# - Xamarin

Xamarin 以前是一家独立的公司。我们六个月之前收购了他们。这是一种使用 C# 来构建跨平台应用的技术,用来制作原生的 Android 和 iOS 应用。它可以让您使用相同的语言、相同的源代码来构建绝大多数应用组件,从而能够为多种不同的移动平台编写应用。

它适用于 iOS 和 Android,同样也可以用在 Mac 之上,顺便提一下,Windows 也可以使用。它可以创建高品质的原生 UI。有许多大型应用正在使用这门技术,因为它可以极大地减少单独在这些平台上编码的工作量。它也允许您使用与后台相同的语言,例如说 Java,不过 Swift 和 Objective-C 还未支持。它以支持的平台量取胜,是一门实现应用的绝佳语言。

Receive news and updates from Realm straight to your inbox

它基于 Mono 项目,这是多年以前从微软离职的员工所实现的开源项目,并且一直维护,致力于能够在其他平台上也能够使用 C#。虽然在微软当中的我们有些固步自封,但是他们却在我们之前看到了 C# 跨平台的潜力,并实现了这个伟大的跨平台项目。Xamarin 正是基于此而生的,您在 iOS 和 Android 应用商店当中看到的许多应用都是基于 C# 的,要么是使用 Xamarin,要么是使用 Unity,这应该是业界领先的游戏引擎。

无处不 C# - Unity

Unity 也是一个基于 Mono 的项目,它的 2D、3D 游戏引擎是用 C# 来编写的。有很多游戏是用 C# 编写的。

无处不 C# - .NET 核心

在微软,我们正努力完善 .NET 核心,这是对整个 .NET ���术栈、运行时以及代码库等内容的全新实现,旨在保证轻量、并且可供服务端使用,并且可用作云和服务器工作负载。它是跨平台的,适用于 Linux、Mac 和 Windows。我们在此之上还放置了 ASP.NET 框架,这是一个被广泛使用的 Web 框架,目前您就可以在非 Windows 的机器上运行 ASP.NET 了,并且它还完全开源了!为什么我们要建立一个单独的核心呢?这将有助于您能够构建更轻量的服务器。

首先,我们移除了 UI 框架,但是 UI 框架能够独立部署。例如,您可以将运行时环境同代码一并发布;云端无需安装各式各样的依赖文件。它拥有一个更优秀的架构,更适合微服务的部署。它同样也致力于使我们的服务端平台更加现代化。这些不同的 .NET 运行在不同的平台上,如果没有统一的部署,那么随着版本的扩散,一切就会变得非常的混乱,尤其是您作为第三方库提供方的时候。您需要某种东西能够跨平台运行,以便解决您的问题。

我们同样也实现了一个名为 「.NET 标准库」的东西,我们提供了一个所有 .NET 平台全兼容的 API。如果您需要使用它的话,您可以直接在工具库当中链接这个 .NET 标准库即可,它将可以在所有平台上运行。您也可以收回在 .NET 生态系统中随处可用代码的能力。我们将随时对标准库进行升级,包括引入新的核心基本库,这样您任意链接所需要的标准库即可。因此 C# 能够在很多地方运行,希望这能够说服大家来尝试 C#,因为这比以前有着更大的适用范围。能够实现这个着实让人兴奋。

Roslyn - C# 语言引擎

我想要多谈论一些底层的内容,也就是谈一谈 Roslyn 项目。

我们对 C# 引擎进行了现代化。以前只有一个简单的 C# 编译器。它使用 C++ 编写的,之后我们有了 Visual Studio,然后将 C# 集成了进去。然而这两者使用 C# 的方式都不同,没有任何的代码共享,因此也没有办法知道对方的相关信息。如果有人想要使用 C# 相关内容的话,那就需要为之编写一套工具,或者为它们自己的 IDE 来添加支持,这是一个很大的工作量。因为人们不得不重头开始编写,因为 C# 引擎对它们而言是个黑盒。这对我们来说也并不是让人满意的。

因此我们决定,是时候重写编译器了,我们不仅重写了 C#,并且还采用了一些新的架构。用来实现 C# 语义的工具只能有一个。我们需要构建一个大家都可以使用的工具,以便满足他们想通过 C# 实现某些功能的愿望。这个工具与平台无关,无论是什么样的平台,比如说批处理过程、编译器,还是某些类似 IDE 的交互式环境,都可以使用这个工具进行。这是一个很难的目标,它花费了我们大量的时间来实现,并且也投入了大量的人力。但是现在,我们推出了 Roslyn API,它切实满足了我们所设定的目标。

大家都需要知道,绝大多数 C# 工具已完成 Roslyn 引擎的迁移,不过仍然有一些没有。场下还有一位演讲者 Ted Brangs 的项目就是例外,因为他是出于某些技术原因的考虑。我们的想法是,这里是您需要用于实现 IDE 和编辑器的代码库。如果您需要使用不同类型的分析工具,那么可以导入它们来对代码中的问题进行分析。如果您想要对代码进行操作的话,例如代码迁移、代码补全或者重构之类的操作,那么您也可以使用它。如果您需要生成源代码,那么您也可以使用它。如果您需要实现更多交互式场景,例如脚本或者 REPL(比如说现在我们在 Visual Studio 当中包含了一个 C# REPL,它是在 Roslyn 的基础上构建的),那么这个引擎仍然能够编译代码并完成输出。

这里是您所能够实现的一个范例,也就是人们能够用 Roslyn 做些什么。这可能会导致编程语言相关的工具呈现爆炸式增长,因为人们现在可以更快地来对 C# 进行处理了。它能够与 C# 很好地协同工作。现在您已经有很多可以轻易得到的信息了,比如说语法、语义等内容,您就可以按照自己的想法,向所需要的地方添加特定的位码了。有一个社区项目充分利用了这点,那就是 OmniSharp。

OmniSharp - 随时随地可编辑 C

OmniSharp 是一个特别的项目,旨在让 C# 能够在您喜爱的编辑器上运行。他们实现这个功能的方法很聪明:由于 C# 现在已经可以在任何地方运行了,因此他们采用 Roslyn C# 引擎,然后在一个单独的进程中运行,不管您在进行开发的机器是什么(例如一台 Mac)。他们使用一个单独的进程来运行引擎,然后剩下所需要做的就是添加一个很简单的集成层,然后加入到特定编辑器中的集成层当中,这样双方便可以通过进程来进行通信,从而让编辑器了解到 C# 的全部信息。

每次您按下了一个键,例如输入了点语法,然后这个时候您需要显示代码补全,这个时候它就会询问旁边的进程:「用户输入了点语法,那么我需要在代码补全列表当中显示什么呢?」。Roslyn 知晓所有的语义,它随后会告诉编辑器:「这五种方法是可用的,显示它们即可」。

通过这种分离方式,使得所有的语义分析过程被包裹在单独的进程当中进行,然后通过标准的数据传输方式来进行数据的交换。这使得我们可以在很多的编辑器当中去实现表现良好的 C#,并且还可以提供语义感知功能(当然,这种做法褒贬不一)。

我需要再提一点,对于微软的 Visual Studio 而言,我们使用 OmniSharp 来实现其 C# 模式,因为这是一个扩展,可以在任何地方加载。它不会内置在编辑器当中。C# 就像其他语言一样,对 Visual Studio 而言就是一个扩展组件。

演示 - Roslyn 分析器

让我们举一个更具体的例子。为了帮助人们将重点放在语言实现之外的地方,我们创建了这个框架,名为 Analyzer,通过它可以轻松地对人们的源代码进行分析、诊断,最后输出结果。这样,我们便可以提供代码修正建议了。

如果您需要:

  • 您的组织有需要强制执行的代码格式
  • 经常执行重构
  • 想要与大家分享代码
  • 需要让代码修正自动化

那么这个工具正是您所需要的。

这个项目您可以在 Visual Studio 当中安装,然后就可以开始使用了。当您打开某个项目的时候,它就已经自带了为这个项目而建立的样板代码。具体而言,当您的项目像这样进入调试模式的时候,然后分析器便会提取您的代码,然后进行代码修正,最后将结果输出。分析器可以以批处理代码的方式运行,也可以将其作为 Nougat 包进行分发。它是以 Visual Studio 扩展程序的身份出现的,在 Visual Studio 的完整版本当中,它是作为调试模式的一部分运行的。现在我运行了 Visual Studio,然后它便开始执行代码修正了。这个是我在完整版本当中的 Visual Studio 所编写的操作。

现在让我们在这个完整版本的 Visual Studio 中打开一些代码。我还没有完全实现完这些分析器的功能。这里是一些我们想要进行操作的示例代码。出于简便起见,我想要实现的东西是语法分析,这里我可以定义各种各样的语义规则。Roslyn 引擎提供了完整的信息供我使用,我可以定义 if 或者 else 语句当中没有柯里化(curlies)语句是非法的代码样式。

我们需要实现那种老式的、固化的代码风格,也就是始终需要添加柯里化的语句,因为接下来编辑代码的时候,并不会得到太多的 BUG。我们需要在某些情况下阻止这种代码的出现,出于时间考虑,我这里只对 if 进行实现,我们当然也可以将其应用到 else 规则当中来。这里让我们来实现一个小型的代码分析器。

这里我不会停止使用这个完整版本。我需要放置一个断点。每当我们看到 if 语句的时候,最开始需要做的就是要注册它,我们需要调用这个方法 AnalyzeNode。每当 Visual Studio 中的源代码分析器命中了一个 if 语句,那么就会自动前往这里,然后我就可以执行一些操作了,随后它就会继续分析代码,直到命中下一个断点。现在我能够得到这段代码当中的全部信息了,接下来我就可以添加操作了。我得到的东西是一个 context 对象。

让我们看一下这个 context 当中的内容。如果这个 if 语句不符合要求,我就可以报告一个诊断过程。我可以得到当前语法树的相关节点,通常就是这个 if 语句当中的内容。让我们开始处理吧,我们将鼠标悬停在这个上面,由于我们位于调试模式,因此这里可以看到一个实际传入的对象。我们可以看到这里的确就是一个 if 语句。让我们使用 Roslyn 构建的对象模型,将其转换为 if 语句模型。

这里我能够得到一个语法树节点,它恰好是 IfStatementSyntax 的派生类。我们可以声明 var ifStatement 赋值为该值。现在这就是我们之后唯一所需要调用的对象了,我这里将不再检查它是否是一个 if 语句。如果 这个语句内部的东西不呈现柯里化的话,那么这个东西将被称为 Block,这就说明这段代码是不符合规范的。如果它不属于 SyntaxKind.Block 的话,接下来我就会提示错误了。我会告诉用户:「这里不对」。现在我需要汇报诊断结果。不过现在我还没有实现,我需要进行一点重构操作,来为之生成一个局部变量。

我可以通过 Diagnostic.Create 创建一个诊断,它需要我提供一些参数。首先是一个被称为 Rule 的描述符,然后我需要指定 location。也就是当问题出现的时候,我需要在代码当中显示波浪线。然后我需要指明当前违背了何种规则。然后指明不符合规则的位置。让我们再对这段实现进行一下重构,生成一个局部变量。这就是我在调试模式全部所需要做的。

那么什么是所谓的「位置」呢?也就是我当前正在处理的这个节点:if 语句。那么我们需要在那里放置提示信息呢?让我们放在这个 if 关键字上吧。if 语句这里拥有一个 if 关键字,因为这是一个具体的语法树。它拥有里面代码的全部实现细节,包括所有的位置。让我们从中取得相关的位置。这里通过 GetLocation 方法来获取。我们得到 if 关键字的位置,然后将这个位置传入到这个方法当中。我编写了一些代码。让我们移除这个断点,然后继续在调试器当中运行。我们等待一会儿,看看发生了什么,好的,现在您会看到 if 语句当中出现了波浪线。

这就是我的全部操作:编写三四行代码就可以识别出问题所在,并告诉框架在哪里显示这个问题。为了向您证明它能够起作用,我把这段代码注释掉,您会发现现在波浪线消失了。

当您试图实现更为复杂的操作的时候,就变得有点困难了,但是这仍然是一个相对简单的语言模型,因为它包含了完整的语法和绑定语义,人们可以用它来构建工具,然后与别人分享,这样便能够让每个人在编辑 C# 的过程当中遵循一致的原则,不管它们所用的编辑器是什么,只要是基于 Roslyn 的就行。无论人们位于哪个平台,他们都能够从中收益匪浅。

我写的这个分析器同样可以在批处理模式当中运行。它可以成为编译过程当中的一部分,可以标记警告或者错误,就如同编译器所做的哪样。我还可以实现修复工具(但我之后的时间不打算演示这个功能),它可以基于我们制定的规则对代码进行修复。

这就是 Roslyn 这边的演示,它是如何帮助人们获得更好的编码体验的,一个更好的 C# 开发体验。它给予了我们更好的代码库,更好的架构,显然在 C# 当中,我们可以对它进行 Dogfood 测试,以更好地帮助我们演进语言本身。

C# 的演进

如今我们对 C# 进行演进较以前已经容易很多了,并且我们可以让社区通过贡献代码而参与到 C# 的演进当中来。然而,这些演进的版本(我不会把所有版本都列述一遍)——展现了我们在创新过程当中的破坏性。

我想以 Async 为例。我们编写了 LINQ,也就是我们在 C# 版本 3 的时候引入的查询操作。我们采取了一个很有趣的冷门语言所存在的概念,并尝试将其主流化,将这些概念引入到 C# 这个主流语言当中,从而帮助它们开扩更广阔的市场。C# 当中的 LINQ 查询就是您能够在其他函数式编程语言当中找到的一个例子。

我们把它们与 Lambda 表达式结合在一起。现在世界上绝大多数语言都可以使用 Lambda 表达式。当然几年以前这并不常见。如今 Java 也可以使用 Lambda 了。当然,我们还是回到 C# 版本 2 来,在这个版本中我们引入了泛型,仅仅只是慢了 Java 一拍,但是泛型已经成为了 C# 必不可少的一部分。因为在 C# 当中,泛型是深嵌入运行时当中的。Java 则是采取了比较谨慎的方法,让泛型直接编译成所需要的东西。

当您将泛型机制实现在运行时当中的时候,这对获取语义而言是 100% 有好处的,然而这同样也意味着性能特性将截然不同,尤其是当语言中存在值类型的时候,当 C# 从版本 1 升级的时候就遇到这个问题,Java 可能会在未来采取这种方式来实现泛型。当语言当中存在值类型的时候,您希望泛型能够识别出它们,并且专门为这些值类型设定特定的规则,也就是在使用泛型的时候就无需对其进行封装和分配内容空间。这样泛型就能够让代码更快,而不是产生拖累。

自我们升级之后,泛型便成为了很多语言的标配特性。由于泛型的引进,我们借此便能够正确地实现查询功能。由于泛型是深嵌入在运行时当中实现的,因此我们可以使用反射机制,这样我们便可以实现一些奇怪的代码引用,然后将 C# 代码转换为 SQL。这都是基于类型可以变换的机理才得以实现,并且甚至在运行时都是可以如此使用。

我们将动态值集成到了静态类型的系统当中,这种类型的类型是动态的、变化的,我们称之为 Dynamic。再次强调,其底层实现仍然是基于泛型来实现的,这使得其运行效率高效,并且避免了很多无谓的封装操作。Async 则是深度依赖于泛型机制。在 C# 版本 6 当中,我们引入了 Roslyn,因此对于实现任何特定的语言特性而言,不再是一件难事。我们现在拥有了更多的灵活性,现在我们便可以实现那些还未实现的微小特性了,我们通过实现这些特性,可以使得开发工作更简单、更优雅、更简洁、更轻松。

之后我们在 C# 版本 6 当中引入了 swath ,也就是现在的 C# 版本。然后在之后的 C# 版本 7 当中,我们将再次引入一些更重要、更底层的特性,我们想要从函数式编程语言当中进行大量借用特性,我认为我们下一步可能是需要引入相关的特性,来处理那些不必用面向对象的方式处理的数据。大家都知道我们一开始是一门非常纯粹的面向对象语言,然后只是倾向于添加一些函数式的特性来作为面向对象的一些扩充,并且我们也在尝试将这两者很好地结合起来。这个灵感来自于 Scala,并且 JVM 也在这么做,尝试将函数式和面向对象结合起来,但是我们绝对不会抛弃面向对象这个根本。

演示:Async

我打算跳过这个 Async 的演示,因为大多数人可能都知道它的作用是什么了。那么让我们来谈一谈即将到来的 C# 版本 7。

C# 版本 7

让我们从元组 (tuple) 开始。首先这里我有一个完整的程序。然后里面有一些数字值。因为数字是非常容易理解的,但是可能不是所有人都能够知晓其中的含义:我们现在拥有的是二进制的字面量。这是一个很微小的特性。当您教孩子编程的时候,这就非常有用了,这些位于数字下方的则是位 (bit)。我现在要再添加一个。我们这里同样还有数字分隔符,就像其他语言所做的那样,您可以将这些分隔符放在需要的地方,从而让数字更容易阅读。

我这里打算实现一个名为 Tally 的方法,它将数组当中的数字累加起来,得出计算结果。这样我们便可以对这些数字调用这个 Tally 方法。当然目前这个方法我还没有实现,让我们使用重构来生成这个方法。这是一个静态方法。目前它的返回值为 Void。或许它应该返回其他的东西。我觉得它应该要返回这个累加值,或者直接返回数字的总数?我觉得两者都是非常重要的。但是现在在 C# 当中您只能返回一样东西,但是在不久的将来,您就可以返回两个返回值了,甚至三个、四个、更多。只要您想,您实际上可以创建一个超大的元组,但是这可能是个糟糕的主意。

现在让我们返回两个 int 值。这是一个元组类型。它表示里面有两个 int 类型,这应该很容易理解。这里是元组字面量,我们还是先返回一些虚拟的值。这里面包含了一些所需的值,通过括号和逗号包裹起来,当然这个语法应该比较正常。当我使用这个方法的时候,我可以获取它的返回值,然后我会发现我得到了一个元组类型。

那么我们该如何使用元组呢?让我们将其输出出来。插入字符串。总和可能位于这里的第一个位置。让我们来看一下元组有些什么:Item1Item2。很显然,我们知道它们分别代表了什么,我们可以直接使用这两个名称。虽然这个名字比较糟糕,但是能用就行。C# 当中的元组还可以为不同的元素赋予不同的名称。这里我将指定各个元素的名称。这是什么意思呢?

当我获取到这个元组的时候,它便可以告诉我它里面的元素是什么。这同样也意味着我来到这里,输入点语法,就可以看到这些预览而已;最终的版本应该需要隐藏掉这些糟糕的名字。这里我们会看到拥有了很显然的名字,因为这里您可以看到 sum,我们可以直接使用它来获取元组当中对应的值。之前那些是底层当中的真实名称,但是编译器知道跟踪这些别名,并使用它们来替代显示。

元组当中拥有元素别名是非常重要的,因为您很可能记不住这个元组是姓在前还是名在前?因此元组需要提供这些信息,这样才能够让人容易理解。您需要有获取别名的能力。

当然,您很可能会希望在获取到元组的时候,就立刻将其解构,将元组当中的值分开,当然您也可以在 C# 当中做到这一点。您可以在这里声明 sum, count,然后元组便会立刻解构为 sum 变量和 count 变量。这样我们便可以不再使用 t. 前缀,而是直接使用 sumcount

让我们现在来实现这个方法。这里我们不再返回一个虚拟值,让我们返回一个真实的结果。这里我们对这些数字执行 foreach 操作;这里我们将其称为 values。接下来我们在每次遍历的时候更新结果值。

result 这个名字太长了,我想将其命名为 r。让我们声明 r = 这个新的元组字面量。这里我希望能够获取旧有的值。我希望 r 同样也有元素别名。让我们前去给其增加别名。您可以在元组字面量当中为其赋别名。这里的语法和命名参数所做的相同。现在 r 拥有了 sc 两个元素别名。我们可以调用 r.s,便可以在这里获取到之前的总和值,然后加上新的值 v,然后这里的 r.c 总数需要加 1。

您可能会在想,这不是很复杂么,很浪费空间。这不是每次都直接分配一个新的数组?或者说在每次遍历的时候偶会创建一个新的元组么?这样做的话,在资源受限的设备或者需要花钱的云端当中是不是很不好?为这些元组分配内存空间是不是非常浪费?

这并不会导致空间的浪费,因为元组不属于对象。元组被实现为值类型,是以 C# 当中的结构体实现的。因此它们不会分配内存空间。它们只是直接更新某些在堆上的东西。这些值类型是使用 copy 来传递的,而不是通过引用来传递。元组没有标识,它们当中仅仅只是包含值而已(我觉得元组应该是这样的)。它们应该只是短暂的存在。因此它们不应该拥有生命周期,这样才能更有效率。

元组不仅是值类型,它们同样也是可变类型。我知道在函数式阵营当中的人们会很反对这种做法,但是我还是坚持元组是可变类型。您可以修改元组里面的值。而不是这样子写:r.s += value。作为一个单独的语句 r.c++ 就很好了,就不用更多的重复了。此外我还可以交换元组当中元素的位置,这并不是危险的操作,因为在线程之间没有共享的可变状态,因为这是一个结构体。没有对象去共享它。您可以随意将其传递到任何地方,它是用拷贝操作执行的,不存在危险的情况。

为什么我们总要强调面向对象呢?为什么一切都必须要封装起来呢?元组没有属性,仅仅只是字段。它们是包含某些可变公共字段的结构体。获取非常简单,您可以很轻松地明白自己的做法。这就是元组的正确用法,因为它们不是抽象类型;它们不会封装任何东西——它们仅仅只是值而已。

关于元组的其他几件事是:由于元组是一种类型,因此它可以判断相等。例如,您可以将元组作为字典当中的键来使用。如果您想要让两个类型都作为某个字典的键,那么使用元组是再好不过的,这样一切都相安无事。哈希值和其他所用的东西在数据结构当中都能够正常的工作。

当然,这也是从异步方法当中获取多个值的绝佳方法,因为如果操作的是异步的话,您可以返回 Task 的元组,这样当所有的操作结束之前,您就可以依次等待。得到元组之后,就可以对其解构并继续前进。元组是很好的传输工具。对于 async 方法以及其他方法而言,如果有多个返回值的话是非常糟糕的,因为您没办法输出多个参数,但是现在通过元组您可以实现这个操作了!

未来展望:更多的模式

我们开始向 C# 当中添加模式匹配 (pattern matching)。

if (o is Point(5, var y)) { WriteLine($"Y: {y}"); } // 递归模式

state = match (state, request) // 匹配表达式,匹配元组
{
    (Closed, Open) => Opened,
    (Closed, Lock) => Locked,
    (Opened, Close) => Closed,
    (Locked, Unlock) => Closed,
};

这里我们从函数式阵营当中引入了一个全新的理念,我们正在逐步实现这个功能。您会在未来的 C# 版本当中见到更多的内容,但是现在让我们跳过这里,介绍一下第一种模式。

让我们把这个例子变成包含递归数字列表的情况。这里我们用的不是 int 数组,而是一个对象数组,其中我们有一个约定,其内部的东西是 int 值或者是其他包含 int 的数组,也可以是新的对象数组,其中有一些 int 值嵌套在当中。或许如果里面也可以包含 null 可能会让人给更加明白,现在我们需要更新一下我们的这个 Tally 方法,让其能够处理这个数组。

首先让我们直接替换为这个对象数组,好的现在我们的得到了一个错误,因为这个 v 不再是 int 类型了;他是一个对象。我们需要知道它是否是 int 值,如果是我们就添加它。因此我们需要进行一些逻辑处理;在过去,我们会执行一个类型检测。如果 vint 类型的话,然后我们就执行转换并处理;但是,即便我们检测出它是 int 类型,这里我们实际上仍然不知道它是什么。我们必须再次执行检查才能将其放入。

相反在这里,您可以将其认为是对 is 表达式的一个扩展。您现在可以声明一个变量。当您询问它是否 is int 的时候,如果是,那么就会取这个 int 值并将其赋到这个新的变量 i 当中。接下来变量 i 就有 v 的值了,但是类型已经确定为 int 了。现在我们就可以在这里将其添加进去,运转良好。

is 表达式现在扩展为允许模式的存在了,这是 C# 当中引入的一个新概念。模式,而不是类型。模式可以是很多复杂的组合。现在模式还不能很复杂。基本上只能够允许常量或者类型模式。例如,可以设定常量值 v is 7(现在这个被允许了,因为这属于常量模式)。我们正在实现更多的模式,将它们集成到语言特性当中来,比如说表达式。

另一个我们正在集成的地方是,我们正在尝试将其整合到 switch 语句当中。我现在可以对任何东西进行 switch,而原来 swtich 只可以对原子类型进行操作。这是很古老的特性了,不过现在它可以对任何东西进行操作了。我们可以对对象进行操作:switch on v。在我的这个 switch 语句当中,我可以列举没有任何常量存在的情况,现在这个属于一种特殊的模式,不过可以对任何模式进行操作。我可以这么声明 case int i。(我必须记得要 break,这就是为什么在这里我得到了一个波浪线)。

我这里已经用了一种模式。我扩展了 switch 语句当中了 case 块,以便其能够应用某种模式,并且可以「当这种模式适用时,就执行此 case 块」。我可以对 swtich 语句进行现代化。我可以让对象数组成为 case 的条件,这也是我所期待的另一件事。让我们将其声明为 a,我可以将条件放到 case 里面。我可以设定「我只需要长度大于 0 的对象 a,因为 a.Length 大于 0(否则就不必执行其他操作了)」。在这种情况下,我可以设定 var t = Tally,然后加入嵌套数组,并将结果添加到 rr = r。您知道后面的用法:r.s + t.sumr.c + t.c。然后 break 退出。这是对既有模式特征的一种泛化,也就是 C# 当中模式匹配所拥有的程度。

在未来,我们希望能够加入更多的模式。我们需要更智能的模式。您应该需要能够使用递归模式。我们让您能够指定一个给定的类型,让其能够被解构。例如,您可以在 Point 类型进行指定,这样它就可以被解构,就像我们之前对元组进行解构,分解为不同的变量里面。当类型被设定为可解构的之后,我们就将其与模式匹配结合在一起,并允许您能够:同时检查 o 是否是 Point 类型,如果是的话就对其进行解构,并且可以应用递归模式:如果 o 是一个 Point,那么这个点的第一个部分 x is 5,然后将第二个部分放到变量 y 当中。您可以得到更智能的模式。您也可以使用它来构造不可读的代码,但是一般而言,如果能够更深入模式,那么您就会发现模式是非常有用的。

我们应该需要在新的地方当中添加模式。switch 语句是 20 世纪 60 年代的产物了。或许我们可以新增一个 switch 语句的表达式版本。或许是一个匹配表达式,而这是函数式语言当中所称呼的,它具有更新的语法,基于表达式,然后 case 语句中也可以添加表达式,这样可以让代码更为简洁。但是现在我们已经有了模式的概念,我们可以添加新的模式,然后向新的地方添加新的模式。这就是我们下一个版本的 C# 所需要关注的一件事,我们已经在努力实现它们,因为 C# 版本 7 已经差不多完成了。(我们还没有发布,也不要问我什么时候发布)。

未来展望:可空的引用类型

string? n; // 可空的引用类型
string  s; // 不可空的引用类型

n = null;  // 允许;它是可空的
s = null;  // 警告!不应该为空
s = n;     // 警告!类型不同

WriteLine(s.Length); // 允许;是不可空的
WriteLine(n.Length); // 警告!它可能为空

if (n != null) { WriteLine(n.Length); } // 允许;您进行了类型检查
WriteLine(n!.Length); // 如果存在的话当然可以

新的语言当中,有一个特性正在成为主流,那就是能够类型系统当中进行区分的能力,也就是判断类型是否可空。

变量有时候可能会为 null,因为它是值域的一部分;但是有些时候我并不希望空值出现,那么为什么我需要随时对引用错误进行处理呢?Swift 也有这项功能。我们能否让 C# 也实现这些功能呢,即便我们现在已经推出了 7 个版本,而可空性完全是一个基于运行时的玩意儿呢?

我们认为可以:我们已经在 C# 当中为可空值类型留下了尾随问号标志。如果我们允许您将它应用于引用类型,或许这就是您陈述某个类型为空的方式。另一方面,如果您不这么声明的话,就说明您期望那里的东西不可能为空。

我们将帮助您进行维护,这意味着我可以将 null 分配给 n,但是不能够分配给 s,并且如果没有任何限定条件的话我也无法将 n 赋值给 s,因为 n 的值很可能是 null。我保护变量以防止它持有不该持有的值。另一方面,当我想要使用这个引用的时候,我可以无需任何限定就执行 s.Length,因为我们知道它可能不会为 null。我们无法让像 C# 之类的语言做一个保证,保证这里一定有值。

n.Length 会警告您它的值可能是 null,您可能会得到 null 引用异常。解决的方法是,这些新语言当中存有一种新的空值检查特性。它们有一种新的模式匹配方法(或者某种可以用来检测 null 的东西)。我们不打算改变您检查 null 的方式。在 C# 当中,已经有 7 种检查空值的方式存在了。相反,我们想让编译器对其进行跟着,来检查某个类型是否为空。

如果您有一个 if 语句,来判断 n 不为空的时候才进入到其中,那么我们就知道这个范围已经经过了类型检查了。事实上它的值不可能为 null,我们将会假设您使用点语法访问内部成员是没有任何问题的。那么有没有别的办法处理它呢?当然有,然而您现在仍然还需要使用这种方式。您必须要随时使用这种方法才能消除所有的空值引用异常。

此外还有一个「强制」操作符,您可以给某个可空值上面加一个前置感叹号 (!),这就意味着您强制让其不能为空。您知道在这里,它的值永远不可能为空,只要你能足够勇敢、足够肯定,那么您就可以使用点语法,也就没有警告的产生。我们已经在开发这个功能,我们希望能够在下一代 C# 当中得到这个功能。

希望这个功能是非常有用的。关于这个特性的一件趣事是:它不仅需要深入到语言内部进行调整,还需要确保我们所有的框架都应用上这个特性,这样您才能够确保自己的代码应用上了正确的可空值。这是一个非常具有挑战性的功能,我觉得这是非常值得的。

About the content

This talk was delivered live in October 2016 at goto; Copenhagen. The video was transcribed by Realm and is published here with the permission of the conference organizers.

Mads Torgersen

Mads is the language PM for C# at Microsoft, where he runs the C# language design process and maintains the language specification. He is also on the design team for TypeScript and Visual Basic, and he contributes to the Roslyn project, which has reinvented the C# and VB compilers and taken them open source.

4 design patterns for a RESTless mobile integration »

close