private static (int Max, int Min) Range(IEnumerable numbers){ int min = int.MaxValue; int max = int.MinValue; foreach(var n in numbers) { min = (n < min) ? n : min; max = (n > max) ? n : max; } return (max, min);}
这里使用元组带来了如下优势:
省事。不需要为返回类型定义 class 或 struct
不需要创建新的类型
增强的语言不再需要调用 方法
方法申明中为返回元组数据的字段提供了名称。调用这个方法的时候,返回的元组会有 Max 和 Min 两个字段:
你也可以为 .NET 中任意类类型提供类似的解构功能,方法是为类提供一个 Deconstruct 成员方法(译者注:称为解构方法,注意与析构方法区分)。Deconstruct 方法提供一组 out 参数,对应于你想解构出来的每一个属性。下面的 Point 类提供了一个解构方法用于提取 X 和 Y 坐标:
public class Point{ public Point(double x, double y) { this.X = x; this.Y = y; } public double X { get; } public double Y { get; } public void Deconstruct(out double x, out double y) { x = this.X; y = this.Y; }}
现在可以通过将 Point 对象赋值到元组来提取各个字段:
var p = new Point(3.14, 2.71);(double X, double Y) = p;
public static int DiceSum3(IEnumerable values){ var sum = 0; foreach (var item in values) { switch (item) { case int val: sum += val; break; case IEnumerable subList: sum += DiceSum3(subList); break; } } return sum;}
匹配表达式的语法与 is 表达式略有不同,是在 case 表达式的开始位置申明类型和变量。
匹配表达式也支持常量,这在遇到简单的条件判断时会节约不少时间:
public static int DiceSum4(IEnumerable values){ var sum = 0; foreach (var item in values) { switch (item) { case 0: break; case int val: sum += val; break; case IEnumerable subList when subList.Any(): sum += DiceSum4(subList); break; case IEnumerable subList: break; case null: break; default: throw new InvalidOperationException("unknown item type"); } } return sum;}
上面的代码添加了 0 作为 int 的特殊情况,null 则是另一个特殊情况,代表没有输入。这演示了 switch 模式表达式中一项重要特性:需要注意 case 表达式的顺序。0 这个条件必须出现在其它 int 条件之前。要不然,int 条件会先匹配到,即使值为 0。如果你搞错了匹配表达式的顺序,一个本应该后匹配到的条件被提前处理了,编译器会标记出来并产生一个错误。
public struct PercentileDie{ public int Value { get; } public int Multiplier { get; } public PercentileDie(int multiplier, int value) { this.Value = value; this.Multiplier = multiplier; }}
然后,添加 case 匹配表达式来处理这种新类型:
public static int DiceSum5(IEnumerable values){ var sum = 0; foreach (var item in values) { switch (item) { case 0: break; case int val: sum += val; break; case PercentileDie die: sum += die.Multiplier * die.Value; break; case IEnumerable subList when subList.Any(): sum += DiceSum5(subList); break; case IEnumerable subList: break; case null: break; default: throw new InvalidOperationException("unknown item type"); } } return sum;}
public static (int i, int j) Find(int[,] matrix, Func predicate){ for (int i = 0; i < matrix.GetLength(0); i++) for (int j = 0; j < matrix.GetLength(1); j++) if (predicate(matrix[i, j])) return (i, j); return (-1, -1); // Not found}
这个代码中有很多问题。首先,它是一返回元组的公共方法,虽然从语法上来说没有问题,但对于公共 API 来说最好是使用用户定义的类型(class 或 struct)。
其次,这个方法返回了矩阵中某项的索引,调用者可以通过这对索引引用矩阵中的元素,并修改其值。
var indices = MatrixSearch.Find(matrix, (val) => val == 42);Console.WriteLine(indices);matrix[indices.i, indices.j] = 24;
public static ref int Find3(int[,] matrix, Func predicate){ for (int i = 0; i < matrix.GetLength(0); i++) for (int j = 0; j < matrix.GetLength(1); j++) if (predicate(matrix[i, j])) return ref matrix[i, j]; throw new InvalidOperationException("Not found");}
现在这个方法返回矩阵某个整数值的引用,你需要在调用的位置对它进行修改。var 申明知道 varItem 现在是 int 而不是元组:
var valItem = MatrixSearch.Find3(matrix, (val) => val == 42);Console.WriteLine(valItem);valItem = 24;Console.WriteLine(matrix[4, 2]);
public static IEnumerable AlphabetSubset(char start, char end){ if ((start < 'a') || (start > 'z')) throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter"); if ((end < 'a') || (end > 'z')) throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter"); if (end <= start) throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}"); for (var c = start; c < end; c++) yield return c;}
检查下面的代码对迭代方法地错误调用:
var resultSet = Iterator.AlphabetSubset('f', 'a');Console.WriteLine("iterator created");foreach (var thing in resultSet) Console.Write($"{thing}, ");
public static IEnumerable AlphabetSubset2(char start, char end){ if ((start < 'a') || (start > 'z')) throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter"); if ((end < 'a') || (end > 'z')) throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter"); if (end <= start) throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}"); return alphabetSubsetImplementation(start, end);}private static IEnumerable alphabetSubsetImplementation(char start, char end){ for (var c = start; c < end; c++) yield return c;}
如果把 alphabetSubsetImplementation 定义为公共 API 方法中的一个局部函数,就清楚多了:
public static IEnumerable AlphabetSubset3(char start, char end){ if ((start < 'a') || (start > 'z')) throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter"); if ((end < 'a') || (end > 'z')) throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter"); if (end <= start) throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}"); return alphabetSubsetImplementation(); IEnumerable alphabetSubsetImplementation() { for (var c = start; c < end; c++) yield return c; }}
public Task PerformLongRunningWork(string address, int index, string name){ if (string.IsNullOrWhiteSpace(address)) throw new ArgumentException(message: "An address is required", paramName: nameof(address)); if (index < 0) throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-negative"); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException(message: "You must supply a name", paramName: nameof(name)); return longRunningWorkImplementation(); async Task longRunningWorkImplementation() { var interimResult = await FirstWork(address); var secondResult = await SecondStep(index, name); return $"The results are {interimResult} and {secondResult}. Enjoy."; }}
注:某些设计使用 Lambda 表达式 来作为局部函数。有兴趣的朋友可以
更多成员支持表达式作为函数体
C# 6 对成员函数和只读属性引入了。C# 7 扩展了允许使用这一特性的成员。在 C# 7 中,可以对构造方法、析构方法、属性的 get 和 set 访问器以及索引使用这一特性,下面是示例:
// Expression-bodied constructorpublic ExpressionMembersExample(string label) => this.Label = label;// Expression-bodied finalizer~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!");private string label;// Expression-bodied get / set accessors.public string Label{ get => label; set => this.label = value ?? "Default label";}
public string Name{ get => name; set => name = value ?? throw new ArgumentNullException(paramName: nameof(value), message: "New name must not be null");}
这个特性也允许在初始化表达式中使用 throw 表达式:
private ConfigResource loadedConfig = LoadConfigResourceOrDefault() ?? throw new InvalidOperationException("Could not load config");
而在以前,这些初始化过程都需要在构造方法中进行,在函数体中使用 throw 语句:
public ApplicationOptions(){ loadedConfig = LoadConfigResourceOrDefault(); if (loadedConfig == null) throw new InvalidOperationException("Could not load config");}
public const int One = 0b0001;public const int Two = 0b0010;public const int Four = 0b0100;public const int Eight = 0b1000;
0b 开始的常量表示它们被写作二进制数。
二进制数可能会很长,所以引入 _ 作为数字分隔符:
public const int Sixteen = 0b0001_0000;public const int ThirtyTwo = 0b0010_0000;public const int SixtyFour = 0b0100_0000;public const int OneHundredTwentyEight = 0b1000_0000;
数字分隔符可以出现在常量中任何地方。对于 10 进制数来说,它们常用作千分位分隔符:
public const long BillionsAndBillions = 100_000_000_000;