【C#】C#8.0常用新特性
经常在第三方.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
}
使用位置模式 来提取 x
和 y
的值。 然后,它使用 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时,是有严格的顺序限制。
- 原文作者:Garfield
- 原文链接:http://www.randyfield.cn/post/2020-06-16-csharp8/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。