初探 C# Script

C#脚本(CSX)是一个用于快速测试C#和.NET代码段的工具,源于VS2015发布的REPL环境。它允许用户在命令行中轻松编写和运行C#脚本,支持引入程序集和其他脚本。文中提供了一个经典的牌组大小判断的示例,并介绍了两种运行CSX脚本的工具:dotnet-script和scriptcs,推荐使用dotnet-script,因为其功能更强大且更新更频繁。最后,文中还提到如何使用传统项目环境进行刷题。


注:以下所有环境均为.net7.0

C# Script(CSX)

C#脚本的根源是伴随着VS2015发布的 C# 读取-求值-打印-循环 (REPL),类似于一个CLI(Command-Line-Interface,命令行)环境,称为REPL CSI。这个CLI环境的发布也顺带引入了将C#脚本化的可能性

以下是官网对C#脚本用途的定义:

C# 脚本是一款用于测试 C# 和 .NET 代码段的工具,无需创建多个单元测试或控制台项目。它提供了轻型选项,可快速在命令行上对 LINQ 聚合方法进行编码、检查 .NET API 是否解压缩文件或调用 REST API,以了解返回的内容或工作原理。它提供了探索和了解 API 的简便方法,无需对 %TEMP% 目录中的另一个 CSPROJ 文件支付开销。

我个人的理解的话就是C#的REPL CSI相当于一个C#版的Python虚拟机,一方面有一个解释型的CLI,一方面支持解释型的脚本。它可能会慢一点,但是对快速测试或者快速实现一个脚本功能是非常便利的。

理论相关参考了这篇文章:必备 .NET - C# 脚本 | Microsoft Learn

编写C#脚本

接下来我们以做算法题为场景学习C#脚本的使用。

C#脚本的规则与C#9中的顶层语句很像,语法也和普通C#完全一致。

主要区别有以下几个:

  • 引入程序集:#r <Assembly.dll>

    • 这里直接按相对路径引入即可
    • VSCode有个奇怪错误就是必须#r System.Console才不会报错,但实际上不影响运行,甚至加了.dll反而会报错
  • 引入其他脚本 #load <script.csx>

    • 个人理解是直接把这些东西像宏一样加载进来
  • 引用命名空间仍然使用 using

下面用经典的牌组大小判断为例展示一下CSX的编写:

main.csx

#r "System.Console"
#load "CardAnalyzer.csx"

string line = "3 4 5 6 7-10 J Q K A";

while ((line = Console.ReadLine()) != null)
{
  var tokens = line.Split('-');
  if (tokens.Length < 2) break;
  var a = GetCardsType(tokens[0]);
  var b = GetCardsType(tokens[1]);
  if (a == CardGroupType.Illegal || b == CardGroupType.Illegal)
  {
    Console.WriteLine("ERROR");
  }
  else if (a == CardGroupType.JokerBomb)
  {
    Console.WriteLine(tokens[0]);
  }
  else if (b == CardGroupType.JokerBomb)
  {
    Console.WriteLine(tokens[1]);
  }
  else if (a == b)
  {
    if (a == CardGroupType.Multiple && tokens[0].Split(' ').Length != tokens[1].Split(' ').Length)
    {
      Console.WriteLine("ERROR");
      continue;
    }
    var aMin = GetBaseCard(tokens[0]);
    var bMin = GetBaseCard(tokens[1]);
    if (aMin > bMin)
      Console.WriteLine(tokens[0]);
    else
      Console.WriteLine(tokens[1]);
  }
  else if (a == CardGroupType.Bomb)
  {
    Console.WriteLine(tokens[0]);
  }
  else if (b == CardGroupType.Bomb)
  {
    Console.WriteLine(tokens[1]);
  }
  else
  {
    Console.WriteLine("ERROR");
  }

}

CardAnalyzer.csx

enum CardGroupType
{
  Single = 0,
  Couple,
  Triple,
  Multiple,
  Bomb,
  JokerBomb,
  Illegal
}

int ParseCard(string card)
{
  int num;
  if (int.TryParse(card, out num))
  {
    return num - 3;
  }
  else
  {
    switch (card)
    {
      case "J":
        return 8;
      case "Q":
        return 9;
      case "K":
        return 10;
      case "A":
        return 11;
      case "joker":
        return 12;
      case "JOKER":
        return 13;
      default:
        return -1;
    }
  }
}

CardGroupType GetCardsType(string token)
{
  string[] cards = token.Split(' ');
  int count = 0;
  int? prev = null;
  bool same = true;
  int lastCard = -1;
  foreach (var card in cards)
  {
    var cardIndex = ParseCard(card);
    if (same && (prev == null || prev == cardIndex))
    {
      count++;
      prev = cardIndex;
      lastCard = cardIndex;
    }
    else if (cardIndex == prev + 1)
    {
      count++;
      prev = cardIndex;
      same = false;
      lastCard = cardIndex;
    }
    else
      return CardGroupType.Illegal;
  }
  if (count == 1) return CardGroupType.Single;
  else if (same && count == 2) return CardGroupType.Couple;
  else if (same && count == 3) return CardGroupType.Triple;
  else if (same && count == 4) return CardGroupType.Bomb;
  else if (!same && count >= 5) return CardGroupType.Multiple;
  else if (count == 2 && lastCard == 13) return CardGroupType.JokerBomb;
  else return CardGroupType.Illegal;
}

int GetBaseCard(string token)
{
  string[] cards = token.Split(' ');
  List<int> cardNums = new List<int>();
  foreach (var card in cards)
  {
    var cardIndex = ParseCard(card);
    cardNums.Add(cardIndex);
  }
  return cardNums.Min();
}

CSX 运行时

有两个工具可以用来运行CSX脚本:

  • dotnet-script:推荐
  • scriptcs

dotnet-script

这是一个官方提供的CSX运行时,提供以下功能:

  • 提供脚本运行环境(dotnet script xxx.csx直接运行脚本)
  • 提供脚手架(dotnet script init),初始化一个VSCode的Debug环境
  • 提供REPL环境:运行dotnet script调出REPL环境
  • 提供系统级直接运行脚本的功能

    • Unix系:在开头加上 #!/usr/bin/env dotnet-script ,直接运行
    • Win:运行dotnet script register来向注册表写入CSX的处理方式
  • 提供内置的Nuget包支持 #r "nuget: PackageName, Version"

它有很多分支(我也不是很懂,我只用第一个),详见下面这个表格,来自于官方仓库:dotnet-script/dotnet-script: Run C# scripts from the .NET CLI. (github.com)

如果有全局.NET 工具支持的话,可以只用.NET CLI输入dotnet tool install -g dotnet-script即可安装(要求.NET Core 2.1 以上)。

scriptcs

这是 CodeRunner 内置的CSX运行命令环境实现,年代相比dotnet-script更久远一点,个人更推荐上一个。

官方仓库:scriptcs/scriptcs: Write C# apps with a text editor, nuget and the power of Roslyn! (github.com)

它也提供类似的功能:

  • 脚本运行环境:scriptcs xxx.csx直接运行脚本
  • 提供REPL环境
  • 提供Nuget包支持,但是方式不如dotnet-script方便

安装是通过Chocolatey进行:cinst scriptcs,优势是直接顶层环境变量。

个人对比下来,scriptcs比dotnet-script要慢一点,功能更少,Nuget引入也没那么方便,最后一次更新也更早,个人更推荐dotnet-script一点。

传统项目作为环境

由于大部分刷题环境不支持CSX,这里还是用传统的CLI方法dotnet new搭配VSCode管理项目搭建刷题环境。

  • 使用 dotnet new console创建一个新的控制台项目

    • 这样在新版本创建的是一个运用顶层语句隐式全局Using的项目
    • 要创建老版本带类的项目可以使用 -use-program-main 参数
    • 消除隐式Using可以在项目配置中取消ImplicitUsing
  • 使用dotnet run直接运行项目
  • 在VSCode中使用C#插件配置launch.jsontasks.json可以实现单步调试

标题: 初探 C# Script

作者: ChlorineC

创建于: 2023-04-10 16:48:00

更新于: 2025-01-05 09:03:00

版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。

Share:

Related Posts

View All Posts »

寻找 ReSharper 开源替代

本文探讨了寻找ReSharper的开源替代品的过程,作者最终选择放弃ReSharper,回归原生Visual Studio,原因包括高昂的授权费用、启动速度慢以及配置复杂。为了改善.NET开发体验,作者从前端开发者的角度对Visual Studio进行了多方面的自定义,包括主题、字体、代码提示、格式化工具等,推荐了SonarLint、CodeRush、Code Maid、CSharpier等插件,以尽可能复刻ReSharper的功能。

我修好了我的博客——如何设计一个全新的博客框架

我修好了我的博客——如何设计一个全新的博客框架

作者分享了个人博客框架迭代历程:从Hexo迁移到Astro的尝试,到发现Elog平台带来的启发,最终思考将Notion作为内容管理系统的新方案。文章重点描述了作者对博客系统的核心需求——解决创作同步问题,以及在探索NotionNext等解决方案过程中遇到的技术限制和思考。

每日一技:VS Code独立配置

使用VS Code的独立配置功能,可以为不同项目创建特定的配置文件,解决了在公司环境中因安全合规要求而无法使用第三方插件的问题。通过配置云同步和工作区单独配置,满足日常开发需求。推荐使用Codeium作为开源解决方案。

Typescript 类型声明 All-in-one

TypeScript 的类型声明文件 .d.ts 提供了 JavaScript 代码的附加元信息,支持类型提示和自动补全。使用 DefinitelyTyped 仓库可以为没有类型声明的 npm 包提供类型支持,类型定义可通过 type 和 interface 进行,declare 语法用于为已有的 JS 变量或函数添加类型。三斜杠指令用于引入额外的文件依赖。