首页 > 编程语言 > C# 8.0 添加和增强的功能【基础篇】

C# 8.0 添加和增强的功能【基础篇】

时间:2023-11-09 16:34:28浏览次数:36  
标签:8.0 Span C# int 添加 var new public

阅读目录

.NET Core 3.x.NET Standard 2.1支持C# 8.0

一、Readonly 成员

可将 readonly 修饰符应用于结构的成员,来限制成员为不可修改状态。这比在C# 7.2中将 readonly 修饰符仅可应用于 struct 声明更精细。

public struct Point
{
    public double X { get; set; }
    public double Y { get; set; }
    public double Distance => Math.Sqrt(X * X + Y * Y);
    public override string ToString() =>
        $"({X}, {Y}) is {Distance} from the origin";

}

与大多数结构一样,ToString() 方法不会修改状态。 可以通过将 readonly 修饰符添加到 ToString() 的声明来对此进行限制:

public readonly override string ToString() =>// 编译器警告,因为 ToString 访问未标记为 readonly 的 Distance 属性
    $"({X}, {Y}) is {Distance} from the origin";
// Distance 属性不会更改状态,因此可以通过将 readonly 修饰符添加到声明来修复此警告

public readonly double Distance => Math.Sqrt(X * X + Y * Y);

注意:readonly 修饰符对于只读属性是必需的。

编译器会假设 get 访问器可以修改状态;必须显式声明 readonly。

自动实现的属性是一个例外;编译器会将所有自动实现的 Getter 视为 readonly,因此,此处无需向 X 和 Y 属性添加 readonly 修饰符。

通过此功能,可以指定设计意图,使编译器可以强制执行该意图,并基于该意图进行优化。
回到顶部
二、默认接口方法

从 .NET Core 3.0 上的 C# 8.0 开始,可以在声明接口成员时定义实现。 最常见的方案是,可以将成员添加到已经由无数客户端发布并使用的接口。示例:

// 先声明两个接口
// 客户接口
public interface ICustomer
{
    IEnumerable<IOrder> PreviousOrders { get; }
    DateTime DateJoined { get; }
    DateTime? LastOrder { get; }
    string Name { get; }
    IDictionary<DateTime, string> Reminders { get; }
    
    // 在客户接口中加入新的方法实现
    public decimal ComputeLoyaltyDiscount()
    {
        DateTime TwoYearsAgo = DateTime.Now.AddYears(-2);
        if ((DateJoined < TwoYearsAgo) && (PreviousOrders.Count() > 10))
        {
            return 0.10m;
        }
        return 0;
    }
}
// 订单接口
public interface IOrder
{
    DateTime Purchased { get; }
    decimal Cost { get; }
}
 
// 测试代码
// SampleCustomer:接口 ICustomer 的实现,可不实现方法 ComputeLoyaltyDiscount
// SampleOrder:接口 IOrder 的实现
SampleCustomer c = new SampleCustomer("customer one", new DateTime(2010, 5, 31))
{
    Reminders =
    {
        { new DateTime(2010, 08, 12), "childs's birthday" },
        { new DateTime(1012, 11, 15), "anniversary" }
    }
};
 
SampleOrder o = new SampleOrder(new DateTime(2012, 6, 1), 5m);
c.AddOrder(o);//添加订单
o = new SampleOrder(new DateTime(2103, 7, 4), 25m);
c.AddOrder(o);
 
// 验证新增的接口方法
ICustomer theCustomer = c; // 从 SampleCustomer 到 ICustomer 的强制转换
Console.WriteLine($"Current discount: {theCustomer.ComputeLoyaltyDiscount()}");

// 若要调用在接口中声明和实现的任何方法,该变量的类型必须是接口类型,即:theCustomer

另可参考大佬文章: C# 8.0 的默认接口方法
回到顶部
三、模式匹配的增强功能

C# 8.0扩展了C# 7.0中的词汇表(is、switch),这样就可以在代码中的更多位置使用更多模式表达式。
3.1 switch 表达式

区别与 switch 语句:

  变量位于 switch 关键字之前;

  将 case 和 : 元素替换为 =>,更简洁、直观;

  将 default 事例替换为 _ 弃元;

  实际语句是表达式,比语句更加简洁。

public static RGBColor FromRainbow(Rainbow colorBand) =>
    colorBand switch
    {
        Rainbow.Red    => new RGBColor(0xFF, 0x00, 0x00),
        Rainbow.Orange => new RGBColor(0xFF, 0x7F, 0x00),
        Rainbow.Yellow => new RGBColor(0xFF, 0xFF, 0x00),
        Rainbow.Green  => new RGBColor(0x00, 0xFF, 0x00),
        Rainbow.Blue   => new RGBColor(0x00, 0x00, 0xFF),
        Rainbow.Indigo => new RGBColor(0x4B, 0x00, 0x82),
        Rainbow.Violet => new RGBColor(0x94, 0x00, 0xD3),
        _              => throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand)),

    };

3.2 属性模式

借助属性模式,可以匹配所检查的对象的属性。

如下电子商务网站的示例,该网站必须根据买家地址(Address 对象的 State 属性)计算销售税。

// Address:地址对象;salePrice:售价
public static decimal ComputeSalesTax(Address location, decimal salePrice) =>
    location switch
    {
        { State: "WA" } => salePrice * 0.06M,
        { State: "MN" } => salePrice * 0.075M,
        { State: "MI" } => salePrice * 0.05M,
        // other cases removed for brevity...
        _ => 0M

    };

此写法,使得整个语法更为简洁。
3.3 元组模式

日常开发中,存在算法依赖于多个输入。使用元组模式,可根据 表示为元组 的多个值进行切换。

// 游戏“rock, paper, scissors(石头剪刀布)”的切换表达式
public static string RockPaperScissors(string first, string second)
    => (first, second) switch
    {
        ("rock", "paper") => "rock is covered by paper. Paper wins.",
        ("rock", "scissors") => "rock breaks scissors. Rock wins.",
        ("paper", "rock") => "paper covers rock. Paper wins.",
        ("paper", "scissors") => "paper is cut by scissors. Scissors wins.",
        ("scissors", "rock") => "scissors is broken by rock. Rock wins.",
        ("scissors", "paper") => "scissors cuts paper. Scissors wins.",
        (_, _) => "tie" // 此处弃元 表示平局(石头剪刀布游戏)的三种组合或其他文本输入

    };

3.4 位置模式

某些类型包含 Deconstruct 方法,该方法将其属性解构为离散变量。 如果可以访问 Deconstruct 方法,就可以使用位置模式检查对象的属性并将这些属性用于模式。

// 位于象限中的 点对象
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,// 当 x 或 y 为 0(但不是两者同时为 0)时,前一个开关中的弃元模式匹配
    _ => Quadrant.Unknown

};

如果没有在 switch 表达式中涵盖所有可能的情况,编译器将生成一个警告。
回到顶部
四、using 声明

using 声明是前面带 using 关键字的变量声明。它指示编译器声明的变量应在封闭范围的末尾进行处理。

static int WriteLinesToFile(IEnumerable<string> lines)
{
    using var file = new System.IO.StreamWriter("WriteLines2.txt");
    int skippedLines = 0;
    foreach (string line in lines)
    {
        if (!line.Contains("Second"))
            file.WriteLine(line);
        else
            skippedLines++;
    }
    return skippedLines;
    // 当代码运行到此位置时,file 被销毁
    // 相当于 using (var file = new System.IO.StreamWriter("WriteLines2.txt")){ ... }

}

 如果 using 语句中的表达式不可用,编译器将生成一个错误。
回到顶部
五、静态本地函数

在C# 8.0中可以向本地函数添加 static 修饰符,以确保本地函数不会从封闭范围捕获(引用)任何变量。 若引用了就会生成报错:CS8421-“静态本地函数不能包含对 <variable> 的引用”。

// 本地方法 LocalFunction 访问了方法 M() 这个封闭空间的变量 y
// 因此,不能用 static 修饰符来声明
int M()
{
    int y;
    LocalFunction();
    return y;
    void LocalFunction() => y = 0;

}

// Add 方法可以是静态的,因为它不访问封闭范围内的任何变量
int M()
{
    int y = 5;
    int x = 7;
    return Add(x, y);
    static int Add(int left, int right) => left + right;

}

回到顶部
六、可处置的 ref 结构

用 ref 修饰符声明的 struct 可能无法实现任何接口,也包括接口 IDisposable。

class Program
{
   static void Main(string[] args)
   {
      using (var book = new Book())
         Console.WriteLine("Hello World!");
   }
}
// 错误写法
// Error CS8343 'Book': ref structs cannot implement interfaces
ref struct Book : IDisposable
{
   public void Dispose()
   {   }
}
// 正确写法
class Program
{
   static void Main(string[] args)
   {
      // 根据 using 新特性,简洁的写法,默认在当前代码块结束前销毁对象 book
      using var book = new Book();
      // ...
    }
}
ref struct Book
{
   public void Dispose()
   {
   }

}

因此,若要能够处理 ref struct,就必须有一个可访问的 void Dispose() 方法。

此功能同样适用于 readonly ref struct 声明。
回到顶部
七、可为空引用类型

若要指示一个变量可能为 null,必须在类型名称后面附加 ?,以将该变量声明为可为空引用类型。否则都被视为不可为空引用类型。

对于不可为空引用类型,编译器使用流分析来确保在声明时将本地变量初始化为非 Null 值。 字段必须在构造过程中初始化。 如果没有通过调用任何可用的构造函数或通过初始化表达式来设置变量,编译器将生成警告。

此外,不能向不可为空引用类型分配一个可以为 Null 的值。

编译器使用流分析,来确保可为空引用类型的任何变量,在被访问或分配给不可为空引用类型之前,都会对其 Null 性进行检查。

string? n = "world";
var x = b ? "Hello" : n; // string?
var y = b ? "Hello" : null; // string? or error

var z = b ? 7 : null; // Error today, could be int?

回到顶部
八、异步流

异步流,可针对流式处理数据源建模 。 数据流经常异步检索或生成元素,因此它们为异步流式处理数据源提供了自然编程模型。

// 异步枚举,核心对象是:IAsyncEnumerable
[HttpGet("syncsale")]
public async IAsyncEnumerable<Product> GetOnSaleProducts()
{
    var products = _repository.GetProducts();
    await foreach (var product in products) // 消费异步枚举,顺序取决于 IAsyncEnumerator 算法
    {
        if (product.IsOnSale)
            yield return product;// 持续异步逐个返回,不用等全部完成
    }

}

另一个实例:模拟异步抓取 html 数据

// 这是一个【相互独立的长耗时行为的集合(假设分别耗时 5,4,3,2,1s)】
static async Task Main(string[] args)
{
      Console.WriteLine(DateTime.Now + $"\tThreadId:{Thread.CurrentThread.ManagedThreadId}\r\n");
      await foreach (var html in FetchAllHtml()) // 默认按照任务加入的顺序输出
      {
           Console.WriteLine(DateTime.Now + $"\tThreadId:{Thread.CurrentThread.ManagedThreadId}\t" + $"\toutput:{html}");
      }
      Console.WriteLine("\r\n" + DateTime.Now + $"\tThreadId:{Thread.CurrentThread.ManagedThreadId}\t");
      Console.ReadKey();
 }
 // 这里已经默认实现了一个 IEnumerator 枚举器: 以 for 循环加入异步任务的顺序
 static async IAsyncEnumerable<string> FetchAllHtml()
 {
    for (int i = 5; i >= 1; i--)
    {
        var html = await Task.Delay(i* 1000).ContinueWith((t,i)=> $"html{i}",i); // 模拟长耗时
        yield return html;
    }

 }

  

接着,其实五个操作是分别开始执行的,那么当耗时短的任务处理好后,能否直接输出呢?这样的话交互体验就更好了!

static async IAsyncEnumerable<string> FetchAllHtml()
{  
    var tasklist= new List<Task<string>>();
    for (int i = 5; i >= 1; i--)
    {
       var t= Task.Delay(i* 1000).ContinueWith((t,i)=>$"html{i}",i);// 模拟长耗时任务
       tasklist.Add(t);
    }
    while(tasklist.Any())  // 监控已完成的操作,立即处理
    {
      var tFinlish = await Task.WhenAny(tasklist);
      tasklist.Remove(tFinlish);
      yield return await tFinlish; // 完成即输出
    }

}  

以上总耗时取决于 耗时最长的那个异步任务5s。

  

  参考自:C# 8.0 宝藏好物 Async streams
回到顶部
九、异步可释放(IAsyncDisposable)

IAsyncDisposable 接口,提供一种用于异步释放非托管资源的机制。与之对应的就是提供同步释放非托管资源机制的接口 IDisposable。

提供此类及时释放机制,可使用户执行资源密集型释放操作,从而无需长时间占用 GUI 应用程序的主线程。

同时更好的完善.NET异步编程的体验,IAsyncDisposable诞生了。它的用法与IDisposable非常的类似:

public class ExampleClass : IAsyncDisposable
{
    private Stream _memoryStream = new MemoryStream();
    public ExampleClass()
    {    }
    public async ValueTask DisposeAsync()
    {
        await _memoryStream.DisposeAsync();
    }
}
// using 语法糖
await using var s = new ExampleClass()
{
    // doing
};
// 优化 同样是对象 s 只存在于当前代码块
await using var s = new ExampleClass();

// doing

  参考于:熟悉而陌生的新朋友——IAsyncDisposable
回到顶部
十、索引和范围

索引和范围,为访问序列中的单个元素或范围,提供了简洁的语法。

新增了两个类型(System.Index & System.Range)和运算符(末尾运算符"^" & 范围运算符“..”)。

用例子说话吧:

var words = new string[]
{
                // index from start    index from end
    "The",      // 0                   ^9
    "quick",    // 1                   ^8
    "brown",    // 2                   ^7
    "fox",      // 3                   ^6
    "jumped",   // 4                   ^5
    
    "over",     // 5                   ^4
    "the",      // 6                   ^3
    "lazy",     // 7                   ^2
    "dog"       // 8                   ^1
    

};              // 9 (or words.Length) ^0

运算实例:

Console.WriteLine($"The last word is {words[^1]}");
// “dog” // 使用 ^1 索引检索最后一个词
 
var quickBrownFox = words[1..4];
//“quick”、“brown”、“fox” 子范围
 
var lazyDog = words[^2..^0];
// “lazy”、“dog” 子范围
 
var allWords = words[..];     
// “The”、“dog”子范围
var firstPhrase = words[..4];
// “The”、“fox”子范围
var lastPhrase = words[6..];  

// “the”、“lazy”、“dog”子范围

另外可将范围声明为变量:

Range phrase = 1..4;

var text = words[phrase];

回到顶部
十一、 Null 合并赋值

Null 合并赋值运算符:??=。

仅当左操作数计算为 null 时,才能使用运算符 ??= 将其右操作数的值分配给左操作数。

List<int> numbers = null;
int? i = null;
numbers ??= new List<int>();
numbers.Add(i ??= 17);
numbers.Add(i ??= 20);
Console.WriteLine(string.Join(" ", numbers));  // output: 17 17

Console.WriteLine(i);  // output: 17

回到顶部
十二、非托管构造类型

在 C# 7.3 及更低版本中,构造类型(包含至少一个类型参数的类型)不能为非托管类型。 从 C# 8.0 开始,如果构造的值类型仅包含非托管类型的字段,则该类型不受管理。

public struct Coords<T>
{
    public T X;
    public T Y;
}
// Coords<int> 类型为 C# 8.0 及更高版本中的非托管类型
// 与任何非托管类型一样,可以创建指向此类型的变量的指针,或针对此类型的实例在堆栈上分配内存块
Span<Coords<int>> coordinates = stackalloc[]
{
    new Coords<int> { X = 0, Y = 0 },
    new Coords<int> { X = 0, Y = 3 },
    new Coords<int> { X = 4, Y = 0 }

};

Span 简介

  在定义中,Span 就是一个简单的值类型。它真正的价值,在于允许我们与任何类型的连续内存一起工作。

  在使用中,Span 确保了内存和数据安全,而且几乎没有开销。

  要使用 Span,需要设置开发语言为 C# 7.2 以上,并引用System.Memory到项目。

  Span 使用时,最简单的,可以把它想象成一个数组,有一个Length属性和一个允许读写的index。

// 常用的一些定义、属性和方法
Span(T[] array);
Span(T[] array, int startIndex);
Span(T[] array, int startIndex, int length);
unsafe Span(void* memory, int length);
int Length { get; }
ref T this[int index] { get; set; }
Span<T> Slice(int start);
Span<T> Slice(int start, int length);
void Clear();
void Fill(T value);
void CopyTo(Span<T> destination);
bool TryCopyTo(Span<T> destination);
// 从 T[] 到 Span 的隐式转换
char[] array = new char[] { 'i', 'm', 'p', 'l', 'i', 'c', 'i', 't' };
Span<char> fromArray = array;
// 复制内存
int Parse(ReadOnlySpan<char> anyMemory);

int Copy<T>(ReadOnlySpan<T> source, Span<T> destination);

  Span 参考: 关于C# Span的一些实践
回到顶部
十三、嵌套表达式中的 stackalloc

从 C# 8.0 开始,如果 stackalloc 表达式的结果为 System.Span<T> 或 System.ReadOnlySpan<T> 类型,则可以在其他表达式中使用 stackalloc 表达式:

Span<int> numbers = stackalloc[] { 1, 2, 3, 4, 5, 6 };
var ind = numbers.IndexOfAny(stackalloc[] { 2, 4, 6, 8 });

Console.WriteLine(ind);  // output: 1

stackalloc 表达式简介:

  stackalloc 关键字用于不安全的代码上下文中,以便在堆栈上分配内存块。

// 关键字仅在局部变量的初始值中有效,正确写法:
int* block = stackalloc int[100];
// 错误写法:
int* block;

block = stackalloc int[100];

  由于涉及指针类型,因此 stackalloc 要求不安全上下文。

  以下代码示例计算并演示 Fibonacci 序列中的前 20 个数字。 每个数字是先前两个数字的和。 在代码中,大小足够容纳 20 个 int 类型元素的内存块是在堆栈上分配的,而不是在堆上分配的。
  该块的地址存储在 fib 指针中。 此内存不受垃圾回收的制约,因此不必将其钉住(通过使用 fixed)。 内存块的生存期受限于定义它的方法的生存期。 不能在方法返回之前释放内存。

class Test
{
    static unsafe void Main()
    {
        const int arraySize = 20;
        int* fib = stackalloc int[arraySize];
        int* p = fib;
        *p++ = *p++ = 1;// The sequence begins with 1, 1.
        for (int i = 2; i < arraySize; ++i, ++p)
            *p = p[-1] + p[-2];// Sum the previous two numbers.
        for (int i = 0; i < arraySize; ++i)
            Console.WriteLine(fib[i]);
        // Keep the console window open in debug mode.
        System.Console.WriteLine("Press any key to exit.");
        System.Console.ReadKey();
    }
}
/*
Output
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765

*/

  不安全代码的安全性低于安全替代代码。 但是,通过使用 stackalloc 可以自动启用公共语言运行时 (CLR) 中的缓冲区溢出检测功能。 如果检测到缓冲区溢出,进程将尽快终止,以最大限度地减小执行恶意代码的机会。

  stackalloc 表达式参考: C#不安全代码和stackalloc
回到顶部
十四、内插逐字字符串的增强功能

内插逐字字符串中 $ 和 @ 标记的顺序可以任意安排:$@"..." 和 @$"..." 均为有效的内插逐字字符串。

在早期 C# 版本中,$ 标记必须出现在 @ 标记之前。

标签:8.0,Span,C#,int,添加,var,new,public
From: https://www.cnblogs.com/shiyh/p/17822136.html

相关文章

  • SPI接口的ADC驱动调试
    SPI接口的ADC驱动调试背景最近在学习IIO子系统,顺带调试了个SPI接口的ADC驱动,所以在这简单记录下。这里只简单介绍了适配一个简单SPI接口ADC驱动的流程,不过多深入框架子系统,更多关于IIO子系统的介绍,请见下一篇:驱动开发dtsdts主要修改或新增以下几点:spimaster控制器相关配置sp......
  • Recurrent Marked Temporal Point Processes: Embedding Event History to Vector
    目录概MotivationMarkedTemporalPointProcess代码DuN.,DaiH.,TrivediR.,UpadhyayU.,Gomez-RodriguzeM.andSongL.Recurrentmarkedtemporalpointprocesses:Embeddingeventhistorytovector.KDD,2016.概利用RNN学习强度函数\(\lambda^*\).在往下......
  • Rust之cargo简单熟悉
    Rust之cargo简单熟悉还记得上一篇文章–《Rust简单开发环境搭建》中,helloworld的例子是用cargo来管理的,今天我们就来聊聊这个cargocargo是什么?为什么需要这个cargo?cargo是Rust的包管理器,Rust的包分为2种,一种是二进制可执行的包,一种是库的包,默认情况下就是第一种binary包在Rust里......
  • 使用docker调试和部署pwn题
    目录使用docker调试和部署pwn题0x0前言0x1调试环境0x2出题模板0x3使用技巧使用docker调试和部署pwn题使用docker快速部署不同架构、不同版本的调试环境。给出docker环境下pwn题部署模板。0x0前言关于docker的基础概念不做过多的介绍。可以到Docker:Accelerated,Conta......
  • Kconfig相关
    Kconfig相关背景以前准备写一下Linux内核里的Kconfig,但一直迟迟没有动手,最近在看Openharmony相关的东西,发现它也可以通过Kconfig和Kconfiglib进行可视化配置,所以想借此机会在这一起记录一下作用#define配置宏主要用来使能/关闭代码,如下面:#defineCONFIG_TEST_ENABLE#ifdefCONF......
  • House of apple 一种新的glibc中IO攻击方法 (2)
    目录Houseofapple一种新的glibc中IO攻击方法(2)前言利用条件利用原理利用思路利用_IO_wfile_overflow函数控制程序执行流利用_IO_wfile_underflow_mmap函数控制程序执行流利用_IO_wdefault_xsgetn函数控制程序执行流例题分析总结Houseofapple一种新的glibc中IO攻击方法(2......
  • [UTCTF2020]sstv
    [UTCTF2020]sstv从题目知道考察sstv,附件给了一个wav文件慢扫描电视(SSTV)慢扫描电视(Slow-scantelevision)是业余无线电爱好者的一种主要图片传输方法,慢扫描电视通过无线电传输和接收单色或彩色静态图片。慢扫描电视的一个术语名称是窄带电视。广播电视需要6MHz的带宽,因为帧速......
  • (十二)C#编程基础复习——break、continue、goto:跳出循环
    在使用循环语句时,并不是必须等待循环完成后才能退出循环,我们也可以主动退出循环,C#为我们提供了break、continue和goto三种方式来跳出循环:1、break它不仅可以用来终止switch语句,在循环语句中使用时还可以用来跳出循环,执行循环外的下一条语句。如果是在嵌套循环中使用,例如在内层的......
  • 简单的C程序示例
        程序调整:程序的输出是否在屏幕上一闪而过?如果遇到这种情况,可以在程序中添加额外的代码,让窗口等待用户按下一个键后关闭。一种方法是,在程序的return语句前添加一行代码。  getchar();这行代码会让程序等待击键,窗口会在用户按下一个键后才关闭。    1、#inclu......
  • 【Django】使用gunicorn部署,找不到静态文件(admin,swagger...)
    先收集静态文件#settings.py里面需要指定收集的路径STATIC_ROOT与STATIC_URLpythonmanage.pycollectstatic添加识别代码#urls.pypath(r'^static/(?P<path>.*)$',serve,{'document_root':STATIC_ROOT}),......