2016 年 1 月

第 31 卷,第 1 期

必备 .NET - C# 脚本

作者:Mark Michaelis | 2016 年 1 月

Mark Michaelis随着 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)\MSBuild\14.0\bin\csi.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.1\lib\net45\Newtonsoft.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。同样,编译在编辑时会实时发生,因此语法错误等将自动加上红色波浪下划线。

使用 Visual Studio C# 交互窗口在类外声明 C# 脚本函数
图 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.0\Common7\IDE\PrivateAssemblies\CSharpInteractive.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.0\Common7\IDE\PrivateAssemblies\SeedUsings.csx 文件:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

正是这两个文件的组合使您可以使用 Console.WriteLine 和 Environment.CurrentDirectory,而不是分别使用完全限定的 System.Console.WriteLine 和 System.Environ-ment.Current­Directory。此外,引用 Microsoft.CSharp 等程序集可支持使用语言功能(如动态语言),无需其他操作。(修改这些文件可更改您的“配置文件”或“首选项”,使更改保存在会话之间。)

有关 C# 脚本语法的更多信息

有关 C# 脚本语法的一个注意事项是,对标准 C# 而言很重要的许多繁琐程序变成了 C# 脚本中适当的可选项。例如,方法体无需出现在函数中,且可以在类范围之外声明 C# 脚本函数。例如,您可以定义在 REPL 窗口中直接显示的 NuGet Install 函数,如图 5 所示。此外,也许有点奇怪,C# 脚本不支持声明命名空间。例如,您无法将拼写类包装在语法命名空间中:命名空间 Grammar { class Spell {} }。

请注意,您可以反复声明相同的构造(变量、类、函数等)。最新声明会覆盖早期声明。

另一个重要注意事项是命令结束分号的行为。语句(如变量分配)需要分号。没有分号,REPL 窗口将继续提示(通过句点)进行更多输入,直到输入分号。另一方面,表达式将在没有分号的情况下执行。因此,System.Diagnostics.Pro­cess.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