因为您可以用,并且也是您的最佳选择!之所以可用,是因为 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# 团队的合作当中获益良多,并且它也给我们提供了很多设计灵感。
(说明:F#是由微软发展的为微软.NET语言提供运行环境的程序设计语言。它是基于Ocaml的,而Ocaml是基于ML函数程序设计语言的。函数编程是解决许多棘手问题的最好方法,但是,纯函数编程并不适合常规编程。因此,函数编程语言逐渐吸收了命令式、面向对象的编程模式,不仅保持了函数编程范式,同时也混合了其他需要的功能,使函数编程编写各种类型的程序都很容易。F# 就是这种尝试的成功代表,比其他函数编程语言做得更多。F#主要是为了解决特定的某些复杂问题,所以本身定位使得VS没有提供F#的ASP.NET/WPF/GDI+的模板,若要使用需要自己配置。所以,一般情况下都是用C#。)
时代在改变 - 为什么要选择 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 还未支持。它以支持的平台量取胜,是一门实现应用的绝佳语言。
它基于 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。
(请记住:C#语言引擎--Roslyn)
OmniSharp - 随时随地可编辑 C#
OmniSharp 是一个特别的项目,旨在让 C# 能够在您喜爱的编辑器上运行。他们实现这个功能的方法很聪明:由于 C# 现在已经可以在任何地方运行了,因此他们采用 Roslyn C# 引擎,然后在一个单独的进程中运行,不管您在进行开发的机器是什么(例如一台 Mac)。他们使用一个单独的进程来运行引擎,然后剩下所需要做的就是添加一个很简单的集成层,然后加入到特定编辑器中的集成层当中,这样双方便可以通过进程来进行通信,从而让编辑器了解到 C# 的全部信息。
每次您按下了一个键,例如输入了点语法,然后这个时候您需要显示代码补全,这个时候它就会询问旁边的进程:「用户输入了点语法,那么我需要在代码补全列表当中显示什么呢?」。Roslyn 知晓所有的语义,它随后会告诉编辑器:「这五种方法是可用的,显示它们即可」。
通过这种分离方式,使得所有的语义分析过程被包裹在单独的进程当中进行,然后通过标准的数据传输方式来进行数据的交换。这使得我们可以在很多的编辑器当中去实现表现良好的 C#,并且还可以提供语义感知功能(当然,这种做法褒贬不一)。
我需要再提一点,对于微软的 Visual Studio 而言,我们使用 OmniSharp 来实现其 C# 模式,因为这是一个扩展,可以在任何地方加载。它不会内置在编辑器当中。C# 就像其他语言一样,对 Visual Studio 而言就是一个扩展组件。
(个人认为:OmniSharp项目对Roslyn引擎进行了封装,以更方便的使第三方编辑器调用,比如自己开发一个C#代码编辑器)
演示 - Roslyn 分析器
让我们举一个更具体的例子。为了帮助人们将重点放在语言实现之外的地方,我们创建了这个框架,名为 Analyzer,通过它可以轻松地对人们的源代码进行分析、诊断,最后输出结果。这样,我们便可以提供代码修正建议了。
如果您需要:
1、您的组织有需要强制执行的代码格式 2、经常执行重构 3、想要与大家分享代码 4、需要让代码修正自动化
那么这个工具正是您所需要的。
这个项目您可以在 Visual Studio 当中安装,然后就可以开始使用了。当您打开某个项目的时候,它就已经自带了为这个项目而建立的样板代码。具体而言,当您的项目像这样进入调试模式的时候,然后分析器便会提取您的代码,然后进行代码修正,最后将结果输出。分析器可以以批处理代码的方式运行,也可以将其作为 Nuget 包进行分发。它是以 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 也在这么做,尝试将函数式和面向对象结合起来,但是我们绝对不会抛弃面向对象这个根本。
(Scala是一门多范式的编程语言,一种类似java的编程语言,设计初衷是实现可伸缩的语言,并集成面向对象编程和函数式编程的各种特性。)
演示:Async
我打算跳过这个 Async 的演示,因为大多数人可能都知道它的作用是什么了。那么让我们来谈一谈即将到来的 C# 版本 7。
(C# 1.0 with Visual Studio.NET C# 2.0 with Visual Studio 2005 C# 3.0 with Visual Studio 2008,2010(.net 2.0, 3.0, 3.5) C# 4.0 with Visual Studio 2010(.net 4) C# 5.0 with Visual Studio 2012(.net 4.5),2013(.net 4.5.1, 4.5.2) C# 6.0 with Visual Studio 2015(.net 4.6, 4.6.1, 4.6.2) C# 7.0 with Visual Studio 2017(.net 4.6.2, 4.7) C#版本是VS版本决定的,也可与相应的.net framework版本对应起来)
C# 版本 7
让我们从元组 (tuple) 开始。首先这里我有一个完整的程序。然后里面有一些数字值。因为数字是非常容易理解的,但是可能不是所有人都能够知晓其中的含义:我们现在拥有的是二进制的字面量。这是一个很微小的特性。当您教孩子编程的时候,这就非常有用了,这些位于数字下方的则是位 (bit)。我现在要再添加一个。我们这里同样还有数字分隔符,就像其他语言所做的那样,您可以将这些分隔符放在需要的地方,从而让数字更容易阅读。
我这里打算实现一个名为 Tally(计数器) 的方法,它将数组当中的数字累加起来,得出计算结果。这样我们便可以对这些数字调用这个 Tally 方法。当然目前这个方法我还没有实现,让我们使用重构来生成这个方法。这是一个静态方法。目前它的返回值为 Void。或许它应该返回其他的东西。我觉得它应该要返回这个累加值,或者直接返回数字的总数?我觉得两者都是非常重要的。但是现在在 C# 当中您只能返回一样东西,但是在不久的将来,您就可以返回两个返回值了,甚至三个、四个、更多。只要您想,您实际上可以创建一个超大的元组,但是这可能是个糟糕的主意。
现在让我们返回两个 int 值。这是一个元组类型。它表示里面有两个 int 类型,这应该很容易理解。这里是元组字面量,我们还是先返回一些虚拟的值。这里面包含了一些所需的值,通过括号和逗号包裹起来,当然这个语法应该比较正常。当我使用这个方法的时候,我可以获取它的返回值,然后我会发现我得到了一个元组类型。
那么我们该如何使用元组呢?让我们将其输出出来。插入字符串。总和可能位于这里的第一个位置。让我们来看一下元组有些什么:Item1 和 Item2。很显然,我们知道它们分别代表了什么,我们可以直接使用这两个名称。虽然这个名字比较糟糕,但是能用就行。C# 当中的元组还可以为不同的元素赋予不同的名称。这里我将指定各个元素的名称。这是什么意思呢?
当我获取到这个元组的时候,它便可以告诉我它里面的元素是什么。这同样也意味着我来到这里,输入点语法,就可以看到这些预览而已;最终的版本应该需要隐藏掉这些糟糕的名字。这里我们会看到拥有了很显然的名字,因为这里您可以看到 sum,我们可以直接使用它来获取元组当中对应的值。之前那些是底层当中的真实名称,但是编译器知道跟踪这些别名,并使用它们来替代显示。
元组当中拥有元素别名是非常重要的,因为您很可能记不住这个元组是姓在前还是名在前。因此元组需要提供这些信息,这样才能够让人容易理解。您需要有获取别名的能力。
当然,您很可能会希望在获取到元组的时候,就立刻将其解构,将元组当中的值分开,当然您也可以在 C# 当中做到这一点。您可以在这里声明 sum, count,然后元组便会立刻解构为 sum 变量和 count 变量。这样我们便可以不再使用 t. 前缀,而是直接使用 sum 和 count。
让我们现在来实现这个方法。这里我们不再返回一个虚拟值,让我们返回一个真实的结果。这里我们对这些数字执行 foreach 操作;这里我们将其称为 values。接下来我们在每次遍历的时候更新结果值。
result 这个名字太长了,我想将其命名为 r。让我们声明 r = 这个新的元组字面量。这里我希望能够获取旧有的值。我希望 r 同样也有元素别名。让我们前去给其增加别名。您可以在元组字面量当中为其赋别名。这里的语法和命名参数所做的相同。现在 r 拥有了 s 和 c 两个元素别名。我们可以调用 r.s,便可以在这里获取到之前的总和值,然后加上新的值 v,然后这里的 r.c 总数需要加 1。
您可能会在想,这不是很复杂么,很浪费空间。这不是每次都直接分配一个新的数组?或者说在每次遍历的时候偶会创建一个新的元组么?这样做的话,在资源受限的设备或者需要花钱的云端当中是不是很不好?为这些元组分配内存空间是不是非常浪费?
这并不会导致空间的浪费,因为元组不属于对象。元组被实现为值类型,是以 C# 当中的结构体实现的。因此它们不会分配内存空间。它们只是直接更新某些在堆上的东西。这些值类型是使用 copy 来传递的,而不是通过引用来传递。元组没有标识,它们当中仅仅只是包含值而已(我觉得元组应该是这样的)。它们应该只是短暂的存在。因此它们不应该拥有生命周期,这样才能更有效率。
元组不仅是值类型,它们同样也是可变类型。我知道在函数式阵营当中的人们会很反对这种做法,但是我还是坚持元组是可变类型。您可以修改元组里面的值。而不是这样子写:r.s += value。作为一个单独的语句 r.c++ 就很好了,就不用更多的重复了。此外我还可以交换元组当中元素的位置,这并不是危险的操作,因为在线程之间没有共享的可变状态,因为这是一个结构体。没有对象去共享它。您可以随意将其传递到任何地方,它是用拷贝操作执行的,不存在危险的情况。
为什么我们总要强调面向对象呢?为什么一切都必须要封装起来呢?元组没有属性,仅仅只是字段。它们是包含某些可变公共字段的结构体。获取非常简单,您可以很轻松地明白自己的做法。这就是元组的正确用法,因为它们不是抽象类型;它们不会封装任何东西——它们仅仅只是值而已。
关于元组的其他几件事是:由于元组是一种类型,因此它可以判断相等。例如,您可以将元组作为字典当中的键来使用。如果您想要让两个类型都作为某个字典的键,那么使用元组是再好不过的,这样一切都相安无事。哈希值和其他所用的东西在数据结构当中都能够正常的工作。
当然,这也是从异步方法当中获取多个值的绝佳方法,因为如果操作的是异步的话,您可以返回 Task 的元组,这样当所有的操作结束之前,您就可以依次等待。得到元组之后,就可以对其解构并继续前进。元组是很好的传输工具。对于 async 方法以及其他方法而言,如果有多个返回值的话是非常糟糕的,因为您没办法输出多个参数,但是现在通过元组您可以实现这个操作了!
(如果需要使用同一类型的多个对象,可以使用集合和数组;如果需要使用不同类型的多个对象,可以使用元组(Tuple)类型。.NET Framework定义了8个泛型Tuple类和一个静态Tuple类,它们用作元组的工厂。元组用静态Tuple类的静态Create()方法创建。Create()方法的泛型参数定义了要实例化的元组类型。)
未来展望:更多的模式
我们开始向 C# 当中添加模式匹配 (pattern matching)。
1 if (o is Point(5, var y)) { WriteLine($"Y: {y}"); } // 递归模式2 3 state = match (state, request) // 匹配表达式,匹配元组4 {5 (Closed, Open) => Opened,6 (Closed, Lock) => Locked,7 (Opened, Close) => Closed,8 (Locked, Unlock) => Closed,9 };
这里我们从函数式阵营当中引入了一个全新的理念,我们正在逐步实现这个功能。您会在未来的 C# 版本当中见到更多的内容,但是现在让我们跳过这里,介绍一下第一种模式。
让我们把这个例子变成包含递归数字列表的情况。这里我们用的不是 int 数组,而是一个对象数组,其中我们有一个约定,其内部的东西是 int 值或者是其他包含 int 的数组,也可以是新的对象数组,其中有一些 int 值嵌套在当中。或许如果里面也可以包含 null 可能会让人能更加明白,现在我们需要更新一下我们的这个 Tally 方法,让其能够处理这个数组。
首先让我们直接替换为这个对象数组,好的现在我们得到了一个错误,因为这个 v 不再是 int 类型了;他是一个对象。我们需要知道它是否是 int 值,如果是我们就添加它。因此我们需要进行一些逻辑处理;在过去,我们会执行一个类型检测。如果 v 是 int 类型的话,然后我们就执行转换并处理;但是,即便我们检测出它是 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,然后加入嵌套数组,并将结果添加到 r;r = r。您知道后面的用法:r.s + t.sum、r.c + t.c。然后 break 退出。这是对既有模式特征的一种泛化,也就是 C# 当中模式匹配所拥有的程度。
在未来,我们希望能够加入更多的模式。我们需要更智能的模式。您应该需要能够使用递归模式。我们让您能够指定一个给定的类型,让其能够被解构。例如,您可以在 Point 类型进行指定,这样它就可以被解构,就像我们之前对元组进行解构,分解为不同的变量里面。当类型被设定为可解构的之后,我们就将其与模式匹配结合在一起,并允许您能够:同时检查 o 是否是 Point 类型,如果是的话就对其进行解构,并且可以应用递归模式:如果 o 是一个 Point,那么这个点的第一个部分 x is 5,然后将第二个部分放到变量 y 当中。您可以得到更智能的模式。您也可以使用它来构造不可读的代码,但是一般而言,如果能够更深入模式,那么您就会发现模式是非常有用的。
我们应该需要在新的地方当中添加模式。switch 语句是 20 世纪 60 年代的产物了。或许我们可以新增一个 switch 语句的表达式版本。或许是一个匹配表达式,而这是函数式语言当中所称呼的,它具有更新的语法,基于表达式,然后 case 语句中也可以添加表达式,这样可以让代码更为简洁。但是现在我们已经有了模式的概念,我们可以添加新的模式,然后向新的地方添加新的模式。这就是我们下一个版本的 C# 所需要关注的一件事,我们已经在努力实现它们,因为 C# 版本 7 已经差不多完成了。(我们还没有发布,也不要问我什么时候发布)。
(此部分关于C#新版本模式的概念理解起来比较模糊,还是待以后版本发布后实际使用一下体现会更贴切。)
未来展望:可空的引用类型
1 string? n; // 可空的引用类型2 string s; // 不可空的引用类型3 n = null; // 允许;它是可空的4 s = null; // 警告!不应该为空5 s = n; // 警告!类型不同6 WriteLine(s.Length); // 允许;是不可空的7 WriteLine(n.Length); // 警告!它可能为空8 if (n != null) { WriteLine(n.Length); } // 允许;您进行了类型检查9 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# 当中得到这个功能。
希望这个功能是非常有用的。关于这个特性的一件趣事是:它不仅需要深入到语言内部进行调整,还需要确保我们所有的框架都应用上这个特性,这样您才能够确保自己的代码应用上了正确的可空值。这是一个非常具有挑战性的功能,我觉得这是非常值得的。
(个人见解:微软在下一个C#版本中增加此定义, 目的还是为了代码的安全性。现在的引用类型在定义时并没有如此分开定义声明,以后在定义如string s这种定义时,可潜移默化的表示是不可以为null的,这样一方面可以不用进行if(s==null)这样的判断;另一方面也同时保证了在忘记进行此类判断时,程序也不会“抛出未将对象引用到定义”等此类的异常。反之,若想定义可以为null的引用类型,则可以以string? s的形式示人,算是微软在语法方面更加规范化了。)
备注:本人基于对原文的理解,增加了个人备注(紫色斜体括号部分),若有错误,请读者提出意见和看法,愿和大家一起进步!
by Mads Torgersen on Dec 27 2016