经常在第三方.NET库中,看到一些“稀奇古怪”的写法,这是啥?没错,这可能就是有所耳闻,但是不曾尝试的C#新语法,本篇就对C#8.0中常用的一些新特性做一个总览,并不齐全,算是抛砖引玉。

1.索引与范围

1.1 索引

使用^操作符:^1指向最后一个元素,^2倒数第二个元素:

char[] vowels = new char[] {'a','e','i','o','u'};
char lastElement  = vowels [^1];   // 'u'
char secondToLast = vowels [^2];   // 'o'

1.2 范围

使用..操作符slice一个数组

左闭右开

ps:是两个点,不是es6的扩展运算符的三个点

char[] vowels = new char[] {'a','e','i','o','u'};
char[] firstTwo =  vowels [..2];    // 'a', 'e'
char[] lastThree = vowels [2..];    // 'i', 'o', 'u'
char[] middleOne = vowels [2..3]    // 'i'
char[] lastTwo =   vowels [^2..];   // 'o', 'u'

1.3 Index类型与Range类型

主要借助于Index类型和Range类型实现索引与范围

char[] vowels = new char[] {'a','e','i','o','u'};
Index last = ^1; 
Range firstTwoRange = 0..2; 
char[] firstTwo = vowels [firstTwoRange];   // 'a', 'e'

1.4 扩展-索引器

可以定义参数类型为Index或Range的索引器

class Sentence
{
  string[] words = "The quick brown fox".Split();
  public string this   [Index index] => words [index];
  public string[] this [Range range] => words [range];
}

2.空合并操作

??=

string s=null;
if(s==null)
    s="Hello,world";//s==null,s is Hello,world,or s is still s

//you can write this
s??="hello,world";

3.using声明

如果省略了using后面的{},及声明语句块,就变为了using declaration,当执行落到所包含的语句块之外时,该资源将被释放

if (File.Exists ("file.txt"))
{
  using var reader = File.OpenText ("file.txt");
  Console.WriteLine (reader.ReadLine());
  ...
}

当执行走到if语句块之外时,reader才会被释放

4.readonly成员

允许在结构体的函数中使用readonly修饰符,确保如果函数试图修改任何字段,会产生编译时错误:

struct Point
{
  public int X, Y;
  public readonly void ResetX() => X = 0;  // Error!
}

如果一个readonly函数调用一个非readonly成员,编译器会产生警告。

public struct Point
{
    public double X { get; set; }
    public double Y { get; set; }
    public double Distance => Math.Sqrt(X * X + Y * Y);

	public readonly override string ToString() =>
    $"({X}, {Y}) is {Distance} from the origin";
}

readonly ToString()调用Distance

warning CS8656: Call to non-readonly member 'Point.Distance.get' from a 'readonly' member results in an implicit copy of 'this'

Distance 属性不会更改状态,因此可以通过将 readonly 修饰符添加到声明来修复此警告

5.静态本地函数

在本地函数加上static,以确保本地函数不会从封闭范围捕获任何变量。 这样做会生成 CS8421,“静态本地函数不能包含对 的引用”。这有助于减少耦合,并使本地方法能够根据需要声明变量,而不会与包含的方法中的变量发生冲突。

int M()
{
    int y = 5;
    int x = 7;
    return Add(x, y);

    static int Add(int left, int right) => left + right;
}

Add()不访问封闭范围中的任何变量,当然如果Add函数访问了本地变量,那就有问题:

int M()
{
    int y;
    LocalFunction();
    return y;

    static void LocalFunction() => y = 0; //warning
}

6.默认接口成员

允许向接口成员添加默认实现,使其成为可选实现:

interface ILogger
{
  void Log (string text) => Console.WriteLine (text);
}

默认实现必须显式接口调用:

class Logger : ILogger { }
...
((ILogger)new Logger()).Log ("message");

接口也可以定义静态成员(包括字段),然后可以在默认实现中访问:

interface ILogger
{
  void Log (string text) => Console.WriteLine (Prefix + text);
  static string Prefix = "";
}

或者在接口外部,因为接口成员是隐式公共的,所以还可以从接口外部访问静态成员的:

ILogger.Prefix = "File log: ";

除非给静态接口成员加上(private,protected,or internal)加以限制,实例字段是禁止的。

7.switch表达式

可以在一个表示式上下文中使用switch

string cardName = cardNumber switch
{
  13 => "King",
  12 => "Queen",
  11 => "Jack",
  _ => "Pip card"   // equivalent to 'default'
};

注意:switch关键字是在变量名之后,{}里面是case子句,要逗号。switch表达式比switch块更简洁,可以使用LINQ查询。

如果忘记了默认表达式_(这个像不像golang的匿名变量),然后switch匹配失败。会抛异常的。

还可以基于元组匹配

int cardNumber = 12;
string suit = "spades";

string cardName = (cardNumber, suit) switch
{
  (13, "spades") => "King of spades",
  (13, "clubs") => "King of clubs",
  ...
};

还可以使用属性模式

System.Uri 类,具有属性Scheme, Host, Port, 和 IsLoopback.考虑如下场景:写一个防火墙,我们可以使用switch表达式的属性模式来决定防火墙的规则(阻止或允许):

bool ShouldAllow (Uri uri) => uri switch
{
  { Scheme: "http",  Port: 80  } => true,
  { Scheme: "https", Port: 443 } => true,
  { Scheme: "ftp",   Port: 21  } => true,
  { IsLoopback: true           } => true,
  _ => false
};

还可以使用位置模式

包含可以访问的解构函数的Point类,可以使用位置模式

public class Point
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y) => (X, Y) = (x, y);

    public void Deconstruct(out int x, out int y) =>
        (x, y) = (X, Y);
}

象限枚举

public enum Quadrant
{
    Unknown,
    Origin,
    One,
    Two,
    Three,
    Four,
    OnBorder
}

使用位置模式 来提取 xy 的值。 然后,它使用 when 子句来确定该点的 Quadrant

static Quadrant GetQuadrant(Point point) => point switch
{
    (0, 0) => Quadrant.Origin,
    var (x, y) when x > 0 && y > 0 => Quadrant.One,
    var (x, y) when x < 0 && y > 0 => Quadrant.Two,
    var (x, y) when x < 0 && y < 0 => Quadrant.Three,
    var (x, y) when x > 0 && y < 0 => Quadrant.Four,
    var (_, _) => Quadrant.OnBorder,
    _ => Quadrant.Unknown
};

8.可空引用类型

使用可空类型,可有效避免NullReferenceExceptions,但是如果编译器认为异常还是可能会出现,就会发出警告。

void Foo (string? s) => Console.Write (s.Length);  // Warning (.Length)

如果需要移除警告,可以使用null-forgiving operator(!)

void Foo (string? s) => Console.Write (s!.Length);

当然上面的例子是很危险的,因为实际上,这个字符串是可能为NULL的。

void Foo (string? s)
{
  if (s != null) Console.Write (s.Length);
}

上面的例子,就不需要!操作符,因为编译器通过静态流分析(static flow analysis`)且足够智能,分析出代码是不可能抛出ReferenceException的。当然编译器分析也不是万能的,比如在数组中,它就不能知道数组元素哪些有数据,哪些没有被填充,所以下面的内容就不会生成警告:

var strings = new string[10];
Console.WriteLine (strings[0].Length);

9.异步流

在C#8.0之前,可以使用yield返回一个迭代器(iterator),或者使用await编写异步函数。但是如果想在一个异步函数返回一个迭代器怎么办?

C#8.0引入了异步流-asynchronous streams,解决了这个问题

//async IAsyncEnumerable
async IAsyncEnumerable<int> RangeAsync (int start, int count, int delay)
{
  for (int i = start; i < start + count; i++)
  {
    await Task.Delay (delay);
    yield return i;
  }
}

await foreach调用异步流

await foreach (var number in RangeAsync (0, 10, 100))
  Console.WriteLine (number);

LINQ查询

这个需要System.Linq.Async

IAsyncEnumerable<int> query =
from i in RangeAsync (0, 10, 500)
  where i % 2 == 0   // Even numbers only.
  select i * 10;     // Multiply by 10.


await foreach (var number in query)
  Console.WriteLine (number);

ASP.Net Core

[HttpGet]
public async IAsyncEnumerable<string> Get()
{
    using var dbContext = new BookContext();
    await foreach (var title in dbContext.Books
                                         .Select(b =>b.Title)
                                         .AsAsyncEnumerable())
    yield return title;
}

可释放

实现 System.IAsyncDisposable 即可,可以使用using自动调用,亦可手动实现IAsyncDisposable接口

10.字符串插值

$@ 标记的顺序可以任意安排:$@"..."@$"..." 均为有效的内插逐字字符串,这个在C#6.0时,是有严格的顺序限制。