作者:Mark Michaelis | 2016 年 1 月
Link: https://msdn.microsoft.com/zh-cn/magazine/mt614271.aspx
随着 Visual Studio 2015 Update 1(下文简称 Update 1)的发布,引出了全新的 C# 读取-求值-打印-循环 (REPL),它可作为 Visual Studio 2015 内的全新交互窗口或新命令行接口 (CLI),称为 CSI。除了将 C# 语言引入命令行外,Update 1 还引入了全新的 C# 脚本语言,以前通常保存到 CSX 文件。
深入探讨全新 C# 脚本之前,必须了解目标场景。C# 脚本是一款用于测试 C# 和 .NET 代码段的工具,无需创建多个单元测试或控制台项目。它提供了轻型选项,可快速在命令行上对 LINQ 聚合方法进行编码、检查 .NET API 是否解压缩文件或调用 REST API,以了解返回的内容或工作原理。它提供了探索和了解 API 的简便方法,无需对 %TEMP% 目录中的另一个 CSPROJ 文件支付开销。
C# REPL 命令行界面 (CSI.EXE)
正如学习 C# 自身,入手学习 C# REPL 界面的最好方法是运行它并开始执行命令。要启动它,从 Visual Studio 2015 开发者命令提示符运行命令 csi.exe,或使用完整路径 C:Program Files (x86)MSBuild14.0incsi.exe。从此处开始执行 C# 语句,如图 1 所示。
图 1 CSI REPL 示例
C:Program Files (x86)Microsoft Visual Studio 14.0>csi
Microsoft (R) Visual C# Interactive Compiler version 1.1.0.51014
Copyright (C) Microsoft Corporation. All rights reserved.
Type "#help" for more information.
> System.Console.WriteLine("Hello! My name is Inigo Montoya");
Hello! My name is Inigo Montoya
>
> ConsoleColor originalConsoleColor = Console.ForegroundColor;
> try{
. Console.ForegroundColor = ConsoleColor.Red;
. Console.WriteLine("You killed my father. Prepare to die.");
. }
. finally
. {
. Console.ForegroundColor = originalConsoleColor;
. }
You killed my father. Prepare to die.
> IEnumerable<Process> processes = Process.GetProcesses();
> using System.Collections.Generic;
> processes.Where(process => process.ProcessName.StartsWith("c") ).
. Select(process => process.ProcessName ).Distinct()
DistinctIterator { "chrome", "csi", "cmd", "conhost", "csrss" }
> processes.First(process => process.ProcessName == "csi" ).MainModule.FileName
"C:\Program Files (x86)\MSBuild\14.0\bin\csi.exe"
> $"The current directory is { Environment.CurrentDirectory }."
"The current directory is C:\Program Files (x86)\Microsoft Visual Studio 14.0."
>
首先注意到的显然是—它类似于 C#—尽管是 C# 新的方言(但整个生产程序没有任何繁琐程序,且在应急原型中是不必要的)。因此,正如您所期望的那样,如果您想要调用静态方法,您可以写出完全限定的方法名称并在圆括号内传递参数。正如在 C# 中,您通过将变量添加为类型的前缀来声明变量,并在声明时选择性地分配给它一个新值。同样,正如您所期望的那样,任何有效方法正文语法—try/catch/finally 块、变量声明、Lambda 表达式和 LINQ—均可无缝运行。
即使在命令行上,其他 C# 功能也可保持,如字符串构造(区分大小写、字符串文本和字符串插值)。因此,当您要使用或输出路径时,需要使用 C# 转义字符 ("") 或字符串文本避开反斜杠,如同 csi.exe 路径输出中的双反斜杠。字符串插值运行方式也如同图 1 演示的"当前目录"示例行,
尽管 C# 脚本支持的远不止语句和表达式。您可以声明自定义类型、通过属性嵌入类型元数据,甚至可以使用特定于 C# 脚本的陈述句简化赘言。考虑图 2 中的拼写检查示例。
图 2 C# 脚本类拼写 (Spell.csx)
#r ".Newtonsoft.Json.7.0.1lib et45Newtonsoft.Json.dll"
#load "Mashape.csx" // Sets a value for the string Mashape.Key
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
public class Spell
{
[JsonProperty("original")]
public string Original { get; set; }
[JsonProperty("suggestion")]
public string Suggestion { get; set; }
[JsonProperty(PropertyName ="corrections")]
private JObject InternalCorrections { get; set; }
public IEnumerable<string> Corrections
{
get
{
if (!IsCorrect)
{
return InternalCorrections?[Original].Select(
x => x.ToString()) ?? Enumerable.Empty<string>();
}
else return Enumerable.Empty<string>();
}
}
public bool IsCorrect
{
get { return Original == Suggestion; }
}
static public bool Check(string word, out IEnumerable<string> corrections)
{
Task <Spell> taskCorrections = CheckAsync(word);
corrections = taskCorrections.Result.Corrections;
return taskCorrections.Result.IsCorrect;
}
static public async Task<Spell> CheckAsync(string word)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(
$"https://montanaflynn-spellcheck.p.mashape.com/check/?text={ word }");
request.Method = "POST";
request.ContentType = "application/json";
request.Headers = new WebHeaderCollection();
// Mashape.Key is the string key available for
// Mashape for the montaflynn API.
request.Headers.Add("X-Mashape-Key", Mashape.Key);
using (HttpWebResponse response =
await request.GetResponseAsync() as HttpWebResponse)
{
if (response.StatusCode != HttpStatusCode.OK)
throw new Exception(String.Format(
"Server error (HTTP {0}: {1}).",
response.StatusCode,
response.StatusDescription));
using(Stream stream = response.GetResponseStream())
using(StreamReader streamReader = new StreamReader(stream))
{
string strsb = await streamReader.ReadToEndAsync();
Spell spell = Newtonsoft.Json.JsonConvert.DeserializeObject<Spell>(strsb);
// Assume spelling was only requested on first word.
return spell;
}
}
}
}
大多数情况下,这只是标准的 C# 类声明。但是,存在多个特定 C# 脚本功能。首先,#r 指令用于引用外部程序集。在本例中,引用的是 Newtonsoft.Json.dll,可帮助分析 JSON 数据。但是请注意,这是一个旨在引用文件系统中文件的指令。同样,它不需要反斜杠转义序列的不必要的繁琐程序。
其次,您可以获得整个列表并将其另存为 CSX 文件,然后使用 #load Spell.csx 将文件"导入"或"内联"到 C# REPL 窗口。#load 指令允许您包括其他脚本文件,犹如所有 #load 文件包括在相同的"项目"或"编译"中。 将代码放入单独的 C# 脚本文件可启用文件重构的类型,更重要的是,能够永久保存 C# 脚本。
使用声明是 C# 脚本中支持的另一个 C# 语言功能,图 2 利用了多次。请注意,与 C# 一样,使用的声明仅限于文件。因此,如果您从 REPL 窗口调用 #load Spell.csx,则将不会保存 Spell.csx 外部正在使用的 Newtonsoft.Json 声明。换言之,如果未在 REPL 窗口中重新进行显式声明,使用 Spell.csx 内的 Newtonsoft.Json 将不会保存到 REPL 窗口(反之亦然)。请注意,也支持使用静态声明的 C# 6.0。因此,"使用静态 System.Console"声明无需将任何 System.Console 成员添加为类型的前缀,支持 REPL 命令,如"WriteLine("Hello! My name is Inigo Montoya")"。
C# 脚本中注释的其他构造包括属性使用、使用语句、属性和函数声明,以及支持 async/await。鉴于后者支持,它甚至可以在 REPL 窗口中利用 await:
(await Spell.CheckAsync("entrepreneur")).IsCorrect
以下是有关 C# REPL 界面的更多注意事项:
- 您不可从 Windows PowerShell 集成脚本编写环境 (ISE) 运行 csi.exe,因为它需要直接控制台输入,而 Windows PowerShell ISE 的"模拟"控制台不支持此操作。(因此,请考虑添加到控制台应用程序的不受支持的列表中—$psUnsupportedConsoleApplications。)
- 不存在离开 CSI 程序的"exit"或"quit"命令。但是,您可以使用 Ctrl+C 结束程序。
- 命令历史记录保存在从同一 cmd.exe 或 PowerShell.exe 会话启动的 csi.exe 会话之间。例如,如果您启动 csi.exe,调用 Console.WriteLine("HelloWorld"),使用 Ctrl+C 退出,然后重新启动 csi.exe,向上箭头键将显示上一个 Console.WriteLine("HelloWorld") 命令。退出 cmd.exe 窗口然后重新启动它将会消除历史记录。
- Csi.exe 支持 #help REPL 命令,这会显示图 3 所示的输出。
- Csi.exe 支持一些命令行选项,如图 4 所示。
图 3 REPL #help 命令输出
> #help
Keyboard shortcuts:
Enter If the current submission appears to be complete, evaluate it.
Otherwise, insert a new line.
Escape Clear the current submission.
UpArrow Replace the current submission with a previous submission.
DownArrow Replace the current submission with a subsequent
submission (after having previously navigated backward).
REPL commands:
#help Display help on available commands and key bindings.
图 4 Csi.exe 命令行选项
Microsoft (R) Visual C# Interactive Compiler version 1.1.0.51014
Copyright (C) Microsoft Corporation. All rights reserved.
Usage: csi [option] ... [script-file.csx] [script-argument] ...
Executes script-file.csx if specified, otherwise launches an interactive REPL (Read Eval Print Loop).
Options:
/help Display this usage message (alternative form: /?)
/i Drop to REPL after executing the specified script
/r:<file> Reference metadata from the specified assembly file
(alternative form: /reference)
/r:<file list> Reference metadata from the specified assembly files
(alternative form: /reference)
/lib:<path list> List of directories where to look for libraries specified
by #r directive (alternative forms: /libPath /libPaths)
/u:<namespace> Define global namespace using
(alternative forms: /using, /usings, /import, /imports)
@<file> Read response file for more options
-- Indicates that the remaining arguments should not be
treated as options
正如前面所说,csi.exe 允许您指定可自定义命令窗口的默认"profile"文件。
- 要消除 CSI 控制台,请调用 Console.Clear。(考虑使用静态 System.Console 声明来添加支持以简化调用 Clear。)
- 如果您要输入多行命令且在之前行中出现错误,您可以使用 Ctrl+Z 然后使用 Enter 以取消并返回未执行的空命令提示符(注意,^Z 将在控制台中显示)。
Visual Studio C# 交互窗口
如上所述,Update 1 中也有全新的 Visual Studio C# 交互窗口,如图 5 所示。C# 交互窗口从"查看 | 其他窗口 | C#"交互菜单启动,打开了一个附加停靠窗口。如同 csi.exe 窗口,它是一个 C# REPL 窗口,但具有一些添加的功能。首先,它包括语法颜色编码和 IntelliSense。同样,编译在编辑时会实时发生,因此语法错误等将自动加上红色波浪下划线。
图 5 使用 Visual Studio C# 交互窗口在类外声明 C# 脚本函数
当然,与 C# 交互窗口的通常关联是 Visual Studio Immediate 和命令窗口。尽管存在重叠部分—但它们都是您可执行 .NET 语句的 REPL 窗口—它们的用途存在巨大差异。C# Immediate 窗口直接绑定到您应用程序的调试上下文,从而允许您将其他语句注入上下文,检查调试会话中的数据,甚至操作和更新数据和调试上下文。同样,命令窗口提供了一个用于操作 Visual Studio 的 CLI,包括执行各种菜单,不过是从命令窗口而不是从菜单自身。(例如,执行命令 View.C#Interactive 将会打开 C# 交互窗口。) 相反,C# 交互窗口允许您执行 C#,包括与上一部分所讨论 C# REPL 界面相关的所有功能。但是,C# 交互窗口无权访问调试上下文。它是完全独立的 C# 会话,没有调试上下文的句柄,甚至对于 Visual Studio 也没有。如同 csi.exe,这个环境无需启动另一个 Visual Studio 控制台或单元测试项目即可让您体验快速 C# 和 .NET 代码段以确认您的理解。无需启动单独的程序,但是 C# 交互窗口托管在 Visual Studio 中,开发者可能已驻留其中。
以下是一些关于 C# 交互窗口的注意事项:
- C# 交互窗口支持许多 csi.exe 中未找到的其他 REPL 命令,包括:
- #cls/#clear,可清除编辑器窗口的内容
- #reset,可将执行环境还原到初始状态,同时保持命令历史记录
- 键盘快捷方式有点出乎意料,如图 6 显示的 #help 输出。
图 6 C# 交互窗口的键盘快捷方式
输入 | 如果当前提交似乎完整,则对其进行评估。否则,插入新行。 |
Ctrl+Enter | 在当前提交内,评估当前提交。 |
Shift+Enter | 插入新行。 |
Escape | 清除当前提交。 |
Alt+UpArrow | 将当前提交替换为上一提交。 |
Alt+DownArrow | 将当前提交替换为随后的提交(之前执行了向后导航后)。 |
Ctrl+Alt+UpArrow | 将当前提交替换为以相同文本开始的上一提交。 |
Ctrl+Alt+DownArrow | 将当前提交替换为以相同文本开始的随后的提交(之前执行了向后导航后)。 |
UpArrow | 在当前提交末尾,将当前提交替换为上一提交。 在其他位置,将游标上移一行。 |
DownArrow | 在当前提交末尾,将当前提交替换为随后的提交(之前执行了向后导航后)。 在其他位置,将游标下移一行。 |
Ctrl+K、Ctrl+Enter | 在交互缓冲区末尾粘贴选择,在输入末尾保留插入点。 |
Ctrl+E、Ctrl+Enter | 在交互缓冲区中任何挂起输入之前,粘贴并执行选择。 |
Ctrl+A | 第一次按下,选择包含游标的提交。第二次按下,选择窗口中的所有文本。 |
请注意,Alt+UpArrow/DownArrow 是撤回命令历史记录的快捷键。Microsoft 选择这些而非更简单的 UpArrow/DownArrow,是因为它希望交互窗口体验符合标准 Visual Studio 代码窗口。
- 因为 C# 交互窗口托管在 Visual Studio 内,所以没有相同的机会使用声明来传递引用,或通过命令行导入,如 csi.exe 一样。但是,C# 交互窗口可从 C:Program Files (x86)Microsoft Visual Studio 14.0Common7IDEPrivateAssembliesCSharpInteractive.rsp 加载其默认的执行上下文,这会默认识别要引用的程序集:
# This file contains command-line options that the C# REPL
# will process as part of every compilation, unless
# "/noconfig" option is specified in the reset command.
/r:System
/r:System.Core
/r:Microsoft.CSharp
/r:System.Data
/r:System.Data.DataSetExtensions
/r:System.Xml
/r:System.Xml.Linq
SeedUsings.csx
此外,CSharpInteractive.rsp 文件引用默认的 C:Program Files (x86)Microsoft Visual Studio 14.0Common7IDEPrivateAssembliesSeedUsings.csx 文件:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
正是这两个文件的组合使您可以使用 Console.WriteLine 和 Environment.CurrentDirectory,而不是分别使用完全限定的 System.Console.WriteLine 和 System.Environ-ment.CurrentDirectory。此外,引用 Microsoft.CSharp 等程序集可支持使用语言功能(如动态语言),无需其他操作。(修改这些文件可更改您的"配置文件"或"首选项",使更改保存在会话之间。)
有关 C# 脚本语法的更多信息
有关 C# 脚本语法的一个注意事项是,对标准 C# 而言很重要的许多繁琐程序变成了 C# 脚本中适当的可选项。例如,方法体无需出现在函数中,且可以在类范围之外声明 C# 脚本函数。例如,您可以定义在 REPL 窗口中直接显示的 NuGet Install 函数,如图 5 所示。此外,也许有点奇怪,C# 脚本不支持声明命名空间。例如,您无法将拼写类包装在语法命名空间中:命名空间 Grammar { class Spell {} }。
请注意,您可以反复声明相同的构造(变量、类、函数等)。最新声明会覆盖早期声明。
另一个重要注意事项是命令结束分号的行为。语句(如变量分配)需要分号。没有分号,REPL 窗口将继续提示(通过句点)进行更多输入,直到输入分号。另一方面,表达式将在没有分号的情况下执行。因此,System.Diagnostics.Process.Start("记事本")将启动记事本,即使没有结束分号。此外,因为 Start 方法调用会返回进程,表达式的字符串输出将显示在命令行上:[System.Diagnostics.Process (Notepad)]。但是,使用分号结束表达式会隐藏输出。因此使用结束分号调用 Start 将不会生成任何输出,即使记事本仍将启动。当然,Console.WriteLine("It would take a miracle."); 仍将输出文本,即使带有分号,因为方法自身将显示输出(而非从方法返回)。
表达式和语句之间的差异有时可能导致细微差别。例如,statement string text = "There's a shortage of perfect b…."; 将导致无输出,但 text="Stop that rhyming and I mean it" 将返回分配的字符串(因为分配会返回已分配的值,且没有分号限制输出)。
用于引用附加程序集 (#r) 和导入现有 C# 脚本 (#load) 的 C# 脚本指令是非常出色的附加功能。(可以想像 project.json 文件等复杂解决方案实现相同的功能,这不会是件高雅的事情。) 不幸的是,编写本文时尚不支持 NuGet 包。要从 NuGet 引用文件,需要将包安装到目录,然后通过 #r 指定引用特定 DLL。(我确信 Microsoft 将会实现这一点。)
请注意,当前指令引用特定文件。例如,您无法在指令中指定变量。尽管您希望通过指令实现此操作,但它无法动态加载程序集。例如,您可以动态调用"nuget.exe install"以提取程序集(再次参见图 5)。但是,这样不会允许将您的 CSX 文件动态绑定到已提取的 NuGet 包,因为无法将程序集路径动态传递给 #r 指令。
C# CLI
我承认我对 Windows PowerShell 爱恨交加。我喜欢命令行上具有 Microsoft .NET Framework 的方便以及可以在管道中传递 .NET 对象,但我不喜欢之前 CLI 中的许多传统文本。即便如此,当涉及到 C# 语言时,我由衷地喜爱它的简洁和强大功能。(至今,我仍对使 LINQ 成为现实的语言扩展印象深刻。) 因此,我应将 Windows PowerShell .NET 的广度与 C# 语言的简洁相结合的想法,意味着我将 C# REPL 作为 Windows PowerShell 的替代品。启动 csi.exe 后,我立即尝试了 cd、dir、ls、pwd、cls、alias 等命令。一言以蔽之,我非常失望,因为这些命令均无法使用。思考体验并与 C# 团队对其进行讨论后,我意识到对于版本 1,团队关注的重点不是替换 Windows PowerShell,而是关注 .NET Framework,因此通过为前面的命令添加您自己的函数,甚至可通过在 Roslyn 上更新 C# 脚本实施,均可支持扩展性。我立即着手为这些命令定义函数。此库的入门教程可从 GitHub 下载:github.com/CSScriptEx。
对于想要寻找功能更强大的 C# CLI(可支持现已可用的先前命令列表)的用户,请考虑 scriptcs.net 上的 ScriptCS(也可从 github.com/scriptcs 的 GitHub 上获得)。它也利用 Roslyn 并包含 alias、cd、clear、cwd、exit、help、install、references、reset、scriptpacks、usings 和 vars。请注意,通过 ScriptCS,命令前缀现在是冒号(如 :reset)而非数字记号(如 #reset)。作为额外奖励,ScriptCS 也以着色和 IntelliSense 形式为 Visual Studio Code 添加了 CSX 文件支持。
总结
至少现在,C# REPL 界面的目的不是替换 Windows PowerShell 甚或 cmd.exe。要想在开始时实现此目的,将会导致失望。当然,我建议您尽可能使 C# 脚本和 REPL CLI 实现 Visual Studio | 新项目的轻型替换: UnitTestProject105 或目的相似的 dotnetfiddle.net。这些是面向 C# 和 .NET 的方法,用于加强您对语言和 .NET API 的理解。C# REPL 提供了对短代码段或程序单元进行编码的方式,您可以即兴使用,直到它们已被剪切并粘贴到大型程序中。它允许您在写入代码时写入语法已验证的更广泛脚本(即便存在大小写不匹配这样的小问题),而不会强制您仅执行脚本以发现输入错误的内容。一旦您知道它的位置,C# 脚本及其交互窗口会变得乐趣无穷,这正是自版本 1.0 起您一直期待的工具。
如同 C# REPL 和 C# 脚本自身一样有趣,认为它们也为成为您自己应用程序的扩展框架提供了跳板—仿照 Visual Basic for Applications (VBA)。通过交互窗口和 C# 脚本支持,您可以想像一个世界—不是太遥远—在这个世界里,您可以将 .NET"宏"再次添加到自己的应用程序中,而无需创建自定义语言、分析器和编辑器。现在,传统 COM 功能正是时候值得引领我们走向新世界。
Mark Michaelis 是 IntelliTect 的创始人,担任首席技术架构师和培训师。在近二十年的时间里,他一直是 Microsoft MVP,并且自 2007 年以来一直担任 Microsoft 区域总监。Michaelis 还是多个 Microsoft 软件设计评审团队(包括 C#、Microsoft Azure、SharePoint 和 Visual Studio ALM)的成员。他在开发者会议上发表了演讲,并撰写了大量书籍,包括最新的"必备 C# 6.0(第 5 版)"(itl.tc/EssentialCSharp)。可通过他的 Facebook facebook.com/Mark.Michaelis、博客 IntelliTect.com/Mark、Twitter @markmichaelis 或电子邮件 mark@IntelliTect.com 与他取得联系。
衷心感谢以下 Microsoft 技术专家对本文的审阅: Kevin Bost 和 Kasey Uhlenhuth