65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 获得了模式匹配、互斥联合、元组和范围

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2016年3月9日

LGPL3

14分钟阅读

viewsIcon

27363

好吧,不是字面意义上的。增强版 C# 支持模式匹配、ADT 和元组,因此普通 C# 也通过传递性获得了所有这些功能。

引言

本文将教你如何使用开源增强版 C# + LeMP 项目的一些“函数式”特性:模式匹配、代数数据类型、元组和数值范围。要下载 Visual Studio 扩展或了解更多关于 LeMP 的信息,请访问 LeMP 主页。 

模式匹配!

有一种代码模式偶尔会出现:“从某处获取一个对象,查看它是否是 X 类型,如果是,则获取/查询其属性”。这会很快变得很繁琐。

var obj = connection.DownloadNextObject();
if (obj is StatusReport)
{
    StatusReport report = (StatusReport)obj;
    if (report.IsValid) {
        SaveReport(report);
    }
}
else if (obj is DataPacket)
{
    DataPacket packet = (DataPacket)obj;
    DoStuffWith(packet);
}

我敢打赌你写过这样的代码。你们中的一些人经常写这种风格的代码。但现在 LeMP 有了一个处理这类模式的快捷方式:它叫做 match。它就像 switch,但用于“模式匹配”。使用 match,上面的代码将变成简单的

match (connection.DownloadNextObject()) {
    case $report is StatusReport(IsValid: true):
        SaveReport(report);
    case $packet is DataPacket:
        DoStuffWith(packet);
}

很简单,对吧?LeMP 会将其翻译成 C# 代码,例如

do {
    var tmp_1 = connection.DownloadNextObject();
    if (tmp_1 is StatusReport) {
        StatusReport report = (StatusReport) tmp_1;
        if (true.Equals(report.IsValid)) {
            SaveReport(report);
            break;
        }
    }
    if (tmp_1 is DataPacket) {
        DataPacket packet = (DataPacket) tmp_1;
        DoStuffWith(packet);
        break;
    }
} while (false);

match 允许 case 块使用 break 来退出每个分支,但与 switch 不同,它不强制要求 break 语句。match 将其输出包装在 do...while(false) 中,以防你的 case 块包含 break 语句(并且,它会在你没有时添加一个 break)。请注意,这里的逻辑与原始 if-else 代码不完全相同:match 的行为就像一个大的“if-else”链,所以如果第一个 case 不匹配,它总是会尝试第二个。所以,如果它是一个 StatusReportIsValid 是 false,它会继续检查它是否是 DataPacket(这听起来可能很蠢,但请仔细想想:对象有可能同时是 StatusReportDataPacket。如果这是不可能的,也许幸运的话编译器会对其进行优化。)

match 还有更多功能,我们稍后会讨论。但首先,来自其赞助商的一句话

代数数据类型!

许多语言提供了由一系列替代项组成的数据类型,例如 Haskell 中一个简单的 二叉搜索树 表示

data BinaryTree t = Leaf t
                  | Node t (BinaryTree t) (BinaryTree t)

这表示 BinaryTree 类型的值是泛型(t 是类型参数,就像 C# 中的 <T> 一样),BinaryTree 值是*要么*是 Leaf(包含一个类型为 t 的单个值),*要么*是 Node(包含一个类型为 t 的值以及两个 BinaryTree t 类型的值)。Haskell 人称之为代数数据类型

代数数据类型(ADT)可能有一个听起来很蠢的名字,但它们也以另一个名字为人所知:互斥联合。

使用 LeMP,你可以使用 alt class 来编写 ADT。你会发现 C# ADT 的工作方式与其他语言中的互斥联合不同;由于其更高的灵活性,它们可能不应该被称为“互斥联合”。但无论你怎么称呼它们,它们都非常有用,尤其是在生产力和简洁代码是你的首要任务时。

在 LeMP 下,ADT 被称为 alt class,它看起来像这样

public abstract alt class BinaryTree<T> where T: IComparable<T>
{
    alt Leaf<T>(T Value);
    alt Node(T Value, BinaryTree<T> Left, BinaryTree<T> Right);
    // (you could also have written `Node<T>`)
}

事实上,这不过是一种非常快速生成不可变类型类层次结构的方法。生成的代码相当庞大

public abstract class BinaryTree<T> where T: IComparable<T>
{
    public BinaryTree() { }
}
class Leaf<T> : BinaryTree<T> where T: IComparable<T>
{
    public Leaf(T Value)
    {
        this.Value = Value;
    }
    public T Value { get; private set; }
    public Leaf<T> WithValue(T newValue)
    {
        return new Leaf<T>(newValue);
    }
    [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
    public T Item1
    {
        get { return Value; }
    }
}
static partial class Leaf
{
    public static Leaf<T> New<T>(T Value) where T: IComparable<T>
    {
        return new Leaf<T>(Value);
    }
}
class Node<T> : BinaryTree<T> where T: IComparable<T>
{
    public Node(T Value, BinaryTree<T> Left, BinaryTree<T> Right)
    {
        this.Value = Value;
        this.Left = Left;
        this.Right = Right;
    }
    public T Value { get; private set; }
    public BinaryTree<T> Left { get; private set; }
    public BinaryTree<T> Right { get; private set; }
    public Node<T> WithValue(T newValue)
    {
        return new Node<T>(newValue, Left, Right);
    }
    public Node<T> WithLeft(BinaryTree<T> newValue)
    {
        return new Node<T>(Value, newValue, Right);
    }
    public Node<T> WithRight(BinaryTree<T> newValue)
    {
        return new Node<T>(Value, Left, newValue);
    }
    // Additional code hidden to spare your eyes
}
static partial class Node
{
    public static Node<T> New<T>(T Value, BinaryTree<T> Left, BinaryTree<T> Right) where T: IComparable<T>
    {
        return new Node<T>(Value, Left, Right);
    }
}

正如你所见,有大量的辅助代码可以使这些类型易于使用。

首先,你不仅会得到一个 Leaf<T>Node<T> 类,还会得到没有类型参数的 LeafNode 类。这些允许你创建叶子和节点而无需提及类型 T

void TreeOfThree()
{
    var tree = Node.New(42, Leaf.New(17), Leaf.New(99));
}

注意:仅当 alt 使用类型参数时,才会创建 New 方法。

你可以使用相应的“With”方法“修改”ADT 的单个属性,如下所示

Node<T> node = Node.New(42, Leaf.New(17), Leaf.New(99));
// Use `WithRight` to change the right child to the leaf `101`:
node = node.WithRight(Leaf.New(101));

当然,你可以使用 match 来了解你的 ADT。例如,如果二叉树已排序,则可以这样搜索

public static bool Contains<T>(BinaryTree<T> tree, T item)
{
    T value;
    match (tree) {
        case is Leaf<T>($value):
            return Compare(value, item) == 0;
        case is Node<T>($value, $left, $right):
            int cmp = Compare(item, value);
            if (cmp < 0)
                return left != null && Contains(left, item);
            else if (cmp > 0)
                return right != null && Contains(right, item);
            else
                return true;
    }
}
internal static int Compare<T>(T a, T b) where T:IComparable<T>
{   // It's null's fault that this method exists.
    if (a != null)
        return a.CompareTo(b);
    else if (b != null)
        return -a.CompareTo(a);
    else
        return 0;
}

请注意,此代码显示的是 Leaf<T>($value) 而不是 Leaf<T>(Value: $value),同样,case is Node<T> 没有提及 ValueLeftRight。如果你省略属性名称,match 将“按位置”从 Item1Item2 等读取项,这就是为什么生成的 Leaf<T> 具有 public T Item1 属性,该属性被标记为 EditorBrowsableState.Never 以在 IntelliSense 中隐藏它。

但是等等,还有更多!事实上,alt class 可以比其他支持 ADT 的语言中的 ADT 做更多的事情,因为它“接受”其作为类层次结构的身份,而不是伪装成传统的数学互斥联合:*它具有与普通类层次结构相同的能力*。

首先,请注意 LeafNode 都具有 Value 属性。我们可以将公共属性移到基类中,如下所示

public partial abstract alt class BinaryTree<T> where T: IComparable<T>
{
    alt this(T Value);
    alt Leaf();
    alt Node(BinaryTree<T> Left, BinaryTree<T> Right);
}

alt this 提供了一种向基类添加数据的方法。此外,如果给它一个 { 花括号 } 主体,它就变成了构造函数中的代码。

现在,由于 Leaf() 除了基类包含的数据之外不包含任何额外数据,我们可以进一步完全消除它,同时从 BinaryTree<T> 中删除 abstract 属性

public alt class BinaryTree<T> where T: IComparable<T>
{
    alt this(T Value);
    alt Node(BinaryTree<T> Left, BinaryTree<T> Right);
}

这确实有一个缺点:叶子节点是用 BinaryTree.New 而不是 Leaf.New 创建的。但它仍然有效。

如果我们想确保 Node 不会用两个 null 子节点初始化,我们可以添加验证代码。这是通过向 Node 添加一个主体来完成的,该主体被视为类主体,然后添加一个名为 alt this 的“构造函数”

public alt class BinaryTree<T> where T: IComparable<T>
{
    alt this(T Value);
    alt Node(BinaryTree<T> Left, BinaryTree<T> Right)
    {
        public alt this() {
            if (Left == null) throw new ArgumentNullException("Left");
            if (Right == null) throw new ArgumentNullException("Right");
        }
    }
}

注意:诚然,这里有些奇怪:新的 this 构造函数有自己的参数列表,即使 Node 已经有一个了。正如你从第一个构造函数(alt this(T Value))中所见,alt class 中的构造函数参数会创建新属性。因此,你可以在 Node 列表或内部 this 列表中放置新属性;建议不要同时使用两者。

另外,与其使用 match,不如将 Contains 方法实现为虚方法。

public alt class BinaryTree<T> where T: IComparable<T>
{
    alt this(T Value);
    alt Node(BinaryTree<T> Left, BinaryTree<T> Right)
    {
        public alt this() {
            if (Left == null && Right == null) throw new ArgumentNullException("Both children");
        }
        public override bool Contains(T item)
        {
            int cmp = Compare(item, Value);
            if (cmp < 0)
                return Left != null && Left.Contains(item);
            else if (cmp > 0)
                return Right != null && Right.Contains(item);
            else
                return true;
        }
    }
    
    public virtual bool Contains(T item)
    {
        return Compare(Value, item) == 0;
    }
    internal static int Compare(T a, T b)
    {   // It's null's fault that this method exists.
        if (a != null)
            return a.CompareTo(b);
        else if (b != null)
            return -a.CompareTo(a);
        else
            return 0;
    }
}

你还可以定义一个多层类层次结构。在下面的示例中,MyTuple<T1,T2> 派生自 MyTuple<T1>,而 MyTuple<T1,T2,T3> 派生自 MyTuple<T1,T2>

public alt class MyTuple<T1> {
    public alt this(T1 Item1);
    public alt MyTuple<T1,T2>(T2 Item2) {
        public alt MyTuple<T1,T2,T3>(T3 Item3) { }
    }
}

更复杂的例子是 GUI 小部件信息的树

public abstract alt class Widget {
    alt this(Rectangle Location) {
        if (Location == null) throw new ArgumentNullException("Location");
    }
    alt Button(string Text) { }
    alt TextBox(string Text) { }
    abstract alt StringListWidget(string[] subItems) {
        alt ComboBox();
        alt ListBox();
    }
    public abstract alt Container() {
        alt TabControl(TabPage[] Children);
        alt Panel(Widget[] Children) {
            alt TabPage(string Title);
        }
    }
}

请记住,嵌套的 alt 不是嵌套类;在输出中,每个新类都被提升到“顶层”。这意味着,例如,你应该写 new ComboBox(location, subItems),而不是 new Widget.StringListWidget.ComboBox(location, subItems)

最后,你可以使用 alt class 来生成只有一个“case”的不可变类,如下所示

public abstract alt class Rectangle {
    alt this(int X, int Y, int Width, int Height);
}

注意:截至撰写本文时,alt class 不支持非公共构造函数。

好的,我想我们已经涵盖了所有内容!希望你喜欢。

元组

增强版 C# 支持元组,例如

var pair = (12, "twelve");

这变成了

var pair = Tuple.Create(12, "twelve");

注意Tuple 是 .NET 框架的标准类。

你也可以解构元组,如下所示

(var num, var str) = pair; 

输出

var num = pair.Item1;
var str = pair.Item2;

由于 ADT 具有名为 Item1Item2 等的属性,因此你可以像这样解构它们

(var x, var y, var w, var h) = new Rectangle(10, 10, 600, 400);

输出

var tmp_0 = new Rectangle(10, 10, 600, 400);
var x = tmp_0.Item1;
var y = tmp_0.Item2;
var w = tmp_0.Item3;
var h = tmp_0.Item4;

然而,EC# 不帮助你编写元组*类型*。例如,要从函数返回 (5,"five"),返回类型是 Tuple<int, string> - 没有快捷方式。

更多模式匹配

使用元组

match 也支持元组和其他具有 Item1Item2 等的对象。

    match (rect) {
    case ($x, $y, $w, $h):
        Console.WriteLine("("+x+","+y+","+w+","+h+")");
    }

但这与上面显示的元组解构没有区别。它完全绕过了类型检查;代码只是

do {
    var x = rect.Item1;
    var y = rect.Item2;
    var w = rect.Item3;
    var h = rect.Item4;
    Console.WriteLine("(" + x + "," + y + "," + w + "," + h + ")");
    break;
} while(false);

这意味着如果 rect 本身类型错误(例如,它是 3 个而不是 4 个的元组),你将收到 C# 编译器的错误。记住,你必须使用 is 来检查数据类型

    match (rect) {
    case is Rectangle($x, $y, $w, $h):
        Console.WriteLine("("+x+","+y+","+w+","+h+")");
    }

不过,一旦你知道了类型,使用元组语法通常很有用。例如,在使用 is Button 后,我们知道我们有一个 Button 小部件,我们知道第一个组件是一个 Rectangle,所以我们可以使用元组语法来解构它,如下所示

void DrawIfButton(Graphics g, Widget widget) {
    match(widget) {
        case is Button(($x, $y, $width, $height), $text):
            // TODO: draw the button
    }
}

简单匹配

模式匹配还能做什么?例如,你可以进行*相等性测试*和*范围测试*,如下所示

static void FavoriteNumberGame()
{
    Console.Write("What's your favorite number? ");
    match(int.Parse(Console.ReadLine())) {
        case 7, 777:  Console.WriteLine("You lucky bastard!");
        case 5, 10:   Console.WriteLine("I have that many fingers too!");
        case 0, 1:    Console.WriteLine("What? Nobody picks that!");
        case 2, 3:    Console.WriteLine("Yeah, I guess you deal with those a lot.");
        case 12:      Console.WriteLine("I prefer a baker's dozen.");
        case 666, 13: Console.WriteLine("Isn't that bad luck though?");
        case 1..<10:  Console.WriteLine("Kind of boring, don't you think?");
        case 11, 13, 17, 19, 23, 29: Console.WriteLine("A prime choice.");
        case 10...99: Console.WriteLine("I have to admit... it has two digits.");
        case _...-1:  Console.WriteLine("Oh, don't be so negative.");
        default:      Console.WriteLine("What are you, high? Like that number?");
    }
}

这个例子说明了几个问题。

单次评估:如你所料,match 会创建一个临时变量,因此 Console.ReadLine() 只会被调用一次。

优先级顺序:较早的 case 在较低的 case 之前进行测试,因此 5 匹配 case 5, 10 而不是 case 1..<10

相等性测试:你不仅可以使用字面量,如 case 5,还可以使用表达式,如 case (x + y):。在输出中,case 5 变成 if (5.Equals(matchExpr))。为什么使用这种特定的相等性测试?考虑替代方案

  • if (matchExpr == 5) 在较少的情况下有效。特别是,考虑 match((object)5) {case 5:}5.Equals((object)5) 返回 true,但 (object)5 == 5 是一个编译时错误。因此 Equals 更灵活。
  • if (object.Equals(matchExpr, 5)) 会涉及装箱和动态向下转换。
  • if (matchExpr.Equals(5))matchExpr 为 null 的情况下会导致 NullReferenceException

只有当你写 case null 时,形式才会改变;这会变成 if (matchExpr == null)注意:除了允许的字面量 null 之外,确保 case 本身不会评估为 null 是你的责任。出于性能和认识论的原因,当你使用非字面量表达式(如 case X)时,match 不会检查 X 本身是否为 null(实际上,它无法判断 X 是否为可空类型)。

默认:与 switch 一样,match 可以有一个 default:,但它必须放在最后。它等同于 case _:

每个 case 的多个模式:增强版 C# 允许 case 有多个*单独*的 case,用逗号分隔,例如 1..10, 20, 30。不幸的是,在将代码翻译成普通 C# 时,两个单独的模式很难指向同一个*处理程序*,因此输出会复制处理程序。例如,上面的第一个 case 被翻译为

if (7.Equals(tmp_0)) {
    Console.WriteLine("You lucky bastard!");
    break;
}
if (777.Equals(tmp_0)) {
    Console.WriteLine("You lucky bastard!");
    break;
}

在这个特定的例子中,*可以*避免复制 Console.WriteLine 语句,但在大多数非平凡的模式中*不行*(至少在 LeMP 不具备的分析能力下不行),所以 match 根本不尝试。因此,如果可能,请避免在带有多个模式的 case 中编写大代码块。

范围运算符:增强版 C# 定义了名为 ..<... 的二元和一元运算符,以及一个名为 in 的二元运算符,该运算符旨在测试值是否包含在集合中。- ..< 是独占范围运算符:它意味着你希望排除范围右侧的数字。case 1..<10 被翻译为类似于 if (tmp_0.IsInRangeExcludeHi(1, 10))IsInRangeExcludeHi 应该是扩展方法,要么是你自己定义的,要么是 Loyc.Essentials.dll 中的一个扩展方法。这个运算符实际上有两个名称:你可以写 1..10,如 Rust 中所用,或者 1..<10,如 Swift 中所用。词法分析器将它们视为同一个运算符(名为 ..)。- ... 是包含范围运算符:它意味着你希望包含范围右侧的数字。例如,case 10...99 被翻译为类似于 if (tmp_0.IsInRange(10, 99))。(这个运算符在 Rust 和 Swift 中也被称为三个点。)

你可以使用下划线来表示一个开放范围,例如 case _..-1..<... 也作为一元运算符存在,所以你可以写 ...-1。但是,没有相应的后缀运算符:你必须写 100..._,而不是 100...。所有这些都被翻译成相应的二元运算符(例如,_...9 变成 matchExpr <= 9)。

一个花哨的例子

这是一个更复杂的例子,展示了 match 的大部分功能

match (obj) {
    case is Shape(ShapeType.Circle, $size, Location: $p is Point<int>($x, $y) && x > y):
        Circle(size, x, y);
}

写这篇文章时,我曾试图解释这是做什么的,但解释太长了,我决定最好只展示输出代码

if (obj is Shape) {
    Shape tmp_0 = (Shape) obj;
    if (ShapeType.Circle.Equals(tmp_0.Item1)) {
        var size = tmp_0.Item2;
        var tmp_1 = tmp_0.Location;
        if (tmp_1 is Point<int>) {
            Point<int> p = (Point<int>) tmp_1;
            var x = p.Item1;
            var y = p.Item2;
            if (x > y) {
                Circle(size, x, y);
                break;
            }
        }
    }
}

你可以在这里看到更多功能

一元和二元“is”运算符is Type 是一个添加到增强版 C# 中的新运算符,专门用于支持模式匹配。它的意思是“检查 match_expression is Type,如果是,则向下转换为 Type 并创建一个临时变量来保存结果”。is 的二元版本允许在左侧有几种不同的情况

  • $newVar is Type 创建一个名为 Type newVar 的新变量来保存向下转换的值
  • ref var is Type 将一个*现有*变量 var 设置为向下转换的值
  • low..hi is Type 将向下转换的值保存在一个临时变量中,然后检查它是否在指定的范围内
  • otherExpr is Type(其中 otherExpr 不匹配以上任何模式)将向下转换的值保存在一个临时变量中,然后检查 otherExpr 是否等于它。

括号内的子模式:在模式的 is 部分之后,你可以在括号中编写“内部模式”或“子模式”。例如,在 case A(B(C), D) 中,A 具有子模式 B(C)D,而 DB 的子模式。每个子模式的处理方式与最外层模式相同,只是子模式可以指定属性名称(例如 Location:),而外层模式不能。

位置和命名属性:在此示例中,Shape 的前两个组件被视为“位置”属性,而第三个组件是“命名”属性(其名称为 Location)。命名属性由标识符后跟冒号 (:) 组成,例如 Location:。如果你不提供名称,match 将使用编号属性(Item1Item2 等)。因此,在此示例中,Shape.Item1 属性与子模式 ShapeType.Circle 进行匹配,而 Shape.Item2$size 进行匹配。

请注意,你只能命名简单属性,而不能命名嵌套属性、方法或索引器属性。例如,你可能会尝试写 case is Foo(Bar(): 777) 来找出 Foo.Bar() 方法是否返回 777,但这不被允许,因为这是语法错误。但是,你可以写 case $foo is Foo && foo.Bar() == 777 来代替。

变量绑定:使用 $ 运算符创建新变量并将其分配给对象的一部分值。在这种情况下,新的 size 变量被分配给 objShape.Item2 属性,新的 p 变量被分配给 Shape.Location,依此类推。

附加条件:你可以在主模式或子模式上使用 && 运算符添加附加条件,例如给定

case is Size(Width: $w, Height: $h && h > 100) && w > h:
    DoSomethingWith(w, h);

输出类似于

if (obj is Size) {
    Size tmp_2 = (Size) obj;
    var w = tmp_2.Width;
    var h = tmp_2.Height;
    if (h > 100 && w > h) {
        DoSomethingWith(w, h);
        break;
    }
}

大致从左到右求值:模式的求值大致是从左到右,但如果你使用的是二元 is 条件,例如 $x is Type,则右侧的类型测试(显然)在左侧的测试或绑定之前运行。

使用“in”运算符的 case

前面你看到你可以写 case lo..hi 来找出值是否在范围内。如果你想将范围测试与变量绑定、相等性测试或子模式匹配结合起来,可以使用 in 运算符。这里有一些例子

match(value) {
    // Is value a double between 0 and 1 ?
    case $newVar is double in 0.0...1.0:
        ZeroToOne(newVar);
    // This one is tricky! It requires that `coefficient.Equals(value) && value in 0...1`
    case coefficient in 0.0...1.0:
        ZeroToOne(newVar);
    // Due to the precedence rules of EC#, if you combine `in` with 
    // subpatterns, the subpatterns must come before `in`.
    case _ is Point(X: $x, Y: $y) in polygon:
        CollisionDetected(x, y);
    // However, when you add conditions with `&&`, they still come last.
    case is Size(Width: $w, Height: $h) in acceptableSizes && w > h:
        SizeIsOK(w, h);
}

使用“ref”为现有变量赋值

你可以使用 ref variable 而不是 $variable 来将值赋给现有变量,而不是创建一个新变量。为了与上一篇文章中引入的 matchCode 宏保持一致,也接受 $(ref variable)

原理

这让我想起来:你认为为什么在 case 中创建新变量的语法是 $x 而不是,比如说,var x?部分原因是 var x 通常不被解析器允许,而且 matchCode 宏也使用 $x 语法,并且 var xmatchCode 的上下文中通常甚至*不可能*。

问你为什么必须写 case $x is Foo 而不是简单地写 case x is Foo,这也是合理的。事实上,起初我确实支持后一种语法(因为 Rust 的工作方式类似),但后来我决定放弃支持。有三个原因

  1. $x 更容易被注意到,所以阅读代码的人可以更容易地看到创建 x 的地方。
  2. $xmatchCode 的语法一致。
  3. 如果你写 777 is FooFoo.Bar is Foo,左侧将被解释为相等性测试。因此,x is Foo 将是一个特例,而且 x is Foox.y is Foo 的作用完全不同,这可能令人惊讶。

独立范围和“in”运算符

..<...in 运算符不限于 match。你可以在普通表达式中使用它们,如下所示

    if (!(index in 0..list.Count))
        throw new ArgumentOutOfRangeException("index");

如前所述,x in lo..<hi 模式被转换为 x.IsInRangeExcludeHi(lo, hi),而 x in lo...hi 模式被转换为 x.IsInRange(lo, hi)。你也可以单独使用 in,或者单独使用范围运算符

    var range = 0..list.Count;
    if (!(index in range))
        throw new ArgumentOutOfRangeException("index");

这被翻译为

    var range = Range.ExcludeHi(0, list.Count);
    if (!range.Contains(index))
        throw new ArgumentOutOfRangeException("index");

Loyc.Essentials.dll 包含此处显示的所有方法;IsInRangeExcludeHiLoyc.Range 类中的一个扩展方法。虽然 Range.ExcludeHiLoyc 命名空间中,但它返回一个 Loyc.Collections.NumRange<Num,Math> 类型的变量,其中 Num 是一个数字类型(如 int),而 Math 是一个辅助类型,允许 NumRange 对该数字类型执行算术运算(这是必需的,因为 .NET 没有为内置类型定义数学接口)。值得注意的是 NumRange 实现 IReadOnlyList<Num>,因此你可以在 foreach 循环和 LINQ 表达式中使用它。

由于表达式 x in range 只是调用 range.Contains(x),因此它与标准集合类型兼容。

总结

我想这就是全部了。希望这些功能能让你更有效率。享受!

要了解更多信息或下载 LeMP,请访问 LeMP 主页

© . All rights reserved.