C# 8.0中的新功能

微信公众号:Fintech极客
作者为软件开发工程师,就职于金融信息科技类公司,通过CFA一级,分享计算机和金融相结合领域的技术和知识。

C# 8.0中的新功能

C# 8.0已经推出来好长一段时间了, 由于公司目前主要使用的还是6.0版本,加上之前个人事情较多,一直没有总结,今天主要查看和测试微软官方文档中的内容:https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-8

只读成员(Readonly members)

在struct前面加上readonly,表示该struct是不可变的,它里面的所有属性都必须是只读的。只读成员使得控制更加精细,也就是说程序员可以指明把哪个成员设置成只读的。下面是一段代码示例:

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

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

        public readonly void TestReadOnly()
        {
            //X = 10;//Complier error: Cannot assign to 'X' because it is read-only
        }
    }

上面的代码片段包含了两个被readonly修饰的函数:ToString和TestReadOnly。在TestReadOnly中,如果试图给X赋值,编译器就会给出注释中的错误信息。
此外,ToString中使用的Distance会导致一个编译警告:CS8656 Call to non-readonly member ‘ReadonlyMembers.Distance.get‘ from a ‘readonly‘ member results in an implicit copy of ‘this‘. 这是因为编译器并不认为get不会改变struct的状态,所以必须显示的设置Distance为readonly,然而对于自动属性则不需要,编译器会认为其get访问器不会修改状态,所以在ToString中访问X,Y,Z成员不会导致编译警告。

默认接口方法(Default interface methods)

在之前的版本中,接口不能包含实现部分,所以一旦在接口中添加新的成员,所有实现该接口的地方都需要修改,而这个新特性使得在添加新成员的时候能给出其实现,从而使得实现接口的地方不需要进行修改。有一点需要注意的是接口的实现并不会像类继承那样继承接口中的实现,所以在调用接口中的实现时,要使用接口类型而不能使用实现类型。注:该功能在 .Net Framewok4.8中不支持,编译错误提示如下图,而在 .Net Core 3.0中是支持的。

下面举一个默认接口实现的例子:

    public interface ICustomer
    {
        string Name { get; set; }

        int OrderCount { get; set; }

        public string GetOrderCount();

        public string GetName()//default implemented method
        {
            return Name;
        }

        protected void PrintName()
        {
            Console.WriteLine("Name:" + Name ?? "");
        }

        private void DummyPrivateMethod()
        {
            Console.WriteLine("Name:" + Name ?? "");
        }
    }

    public class FintechCustomer : ICustomer
    {
        public string Name { get; set; }
        public int OrderCount { get; set; } = 100;

        public string GetOrderCount()
        {
            return $"FintechCustomer order count:{OrderCount}";
        }

        public string GetName()//override default implemented method
        {
            return $"FintechCustomer Name:{Name}";
        }

        public FintechCustomer(string name) => Name = name;

    }

    public class NormalCustomer : ICustomer
    {
        public string Name { get; set; }
        public int OrderCount { get; set; }

        public string GetOrderCount()
        {
            return $"Normal customer order count:{OrderCount}";
        }

        public NormalCustomer(string name)
        {
            Name = name;
            OrderCount = 0;
        }
    }

上述代码中包含了一个接口ICustomer和两个实现类FintechCustomer,NormalCustomer。有以下几点需要注意:

  1. ICustomer接口提供了GetName的具体实现
  2. FintechCustomer类提供了自己的GetName实现,从而覆盖了ICustomer中的GetName
  3. NormalCustomer类没有提供自己的GetName实现,但也没有继承ICustomer中的GetName,通下面的调用示例,我们可以验证这点
  4. 在ICustomer接口内部,实现成员可以加各种修饰符如:private, protected, public等,但它们都不会被继承。

代码调用示例如下:

    public class DefaultInterfaceImplementationUnitTest
    {
        [TestMethod]
        public void FintechCustomer_TestGetName()
        {
            var name = "FintechCustomer1";
            var fc = new FintechCustomer(name);
            ICustomer ic = fc;
            Assert.AreEqual($"FintechCustomer Name:{name}", fc.GetName());
            Assert.AreEqual($"FintechCustomer Name:{name}", ic.GetName());
        }

        [TestMethod]
        public void NormalCustomer_TestGetName()
        {
            var name = "NormalCustomer1";
            var nc = new NormalCustomer(name);
            ICustomer ic = nc;
            //Assert.AreEqual(name, nc.GetName());//编译错误: Error CS1061  'NormalCustomer' does not contain a definition for 'GetName'...
            Assert.AreEqual(name, ic.GetName());
        }
    }

在NormalCustomer_TestGetName函数中,被注释掉的那行显示出,实现类NormalCustomer的引用是无法调用在接口ICustomer中实现的函数GetName的,只能通过ICustomer的引用调用GetName。然而,FintechCustomer_TestGetName中,我们可以看到无论是FintechCustomer还是ICustomer的引用都可以调用GetName函数,并且调用的都是FintechCustomer中的函数。

更多的模式匹配

C# 7.0通过is和switch引入了类型和常量匹配,C# 8.0中扩展了使用它们的地方。这些特性主要是为了支持数据与功能相分离的编程范式,当数据和功能是分开的或者算法并不依赖于运行时对象的类型时,可以考虑使用这些特性,它们提供了另一种表达设计的方式。下面会讲到switch表达式和property,tuple,postition三种模式匹配。

switch表达式(Switch Expression)

先看一个例子

        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)),
            };

        public enum Rainbow
        {
            Red,
            Orange,
            Yellow,
            Green,
            Blue,
            Indigo,
            Violet
        }

从上面例子中的FromRainbow,我们可以看到switch表达式相较于switch语句发生了很大的变化,主要在以下四点:

  1. 变量的名字(colorBand)放在了switch关键字前面,这样编译器就能容易switch语句和switch表达式。
  2. =>取代了case 和 :并且每一句的结尾用的是,而非;,这样更加的简约和直观。
  3. default分支被_取代。
  4. 整块代码是一个表达式而非语句。

最后,switch表达式必须返回一个值或者抛出异常,如果一次调用没有case匹配上,则会抛出InvalidOperation异常,编译器也会在switch分支不包含所有情况的时候给出警告。

属性模式(Property Pattern)

下面还是从一个例子讲起

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

        public class Address
        {
            public string State { get; set; }
        }

上面代码中的ComputeSalesTax函数用到了switch表达式,而switch表达式内部的匹配则用到了属性模式。例如下面的一句表示:如果location的State属性值为"WA",那么就返回salePrice * 0.06M:

 { State: "WA" } => salePrice * 0.06M,

元组模式(tuple pattern)

有些算法要求多个输入,而元组模式允许在switch表达式中使用的tuple来进行多个值的匹配,下面是一个例子:

    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"
    };

上面是剪刀石头布的游戏,可以看到switch表达式使用了元组还匹配只返回那条消息,其中(,)=>"tie"表示未找到任何匹配时,返回"tie"。

位置模式(Positional patterns)

C# 7.0引入了Deconstruct函数,如果一个类中还有Deconstrut函数,那么就可以直接把该类的属性分解到不同的变量中。在C# 8.0中,可以通过位置模式在switch表达式中进行多个属性的匹配,下面是一个例子:

        public 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
        };

        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);
        }

本例中,Point类包含了Deconstruct函数,从而可以把它的X,Y分解到单独的变量中。在GetQuadrant函数中,把point对象通过Deconstruct函数分解到x,y两个变量中,并通过when来判断是否满足该分支条件。

using声明

在之前的版本中,我们可以使用using语句来保证实现了IDisposable的对象在生命周期结束后,其Dispose方法被调用。在C# 8.0中,我们using声明来做同样的事并且code更加紧凑。下面是一个打开文件的例子:

static int WriteLinesToFile(IEnumerable<string> lines)
{
    using var file = new System.IO.StreamWriter("WriteLines2.txt");
    // Notice how we declare skippedLines after the using statement.
    int skippedLines = 0;
    foreach (string line in lines)
    {
        if (!line.Contains("Second"))
        {
            file.WriteLine(line);
        }
        else
        {
            skippedLines++;
        }
    }
    // Notice how skippedLines is in scope here.
    return skippedLines;
    // file is disposed here
}

上例中,在函数调用返回时会自动调用file对象的Dispose方法,如果using声明的对象没有实现IDisposable,编译器会生成错误信息。

静态本地函数(Static local functions)

如果不想本地函数访问即捕获它外面的本地变量,我们可以加上static进行限制,下面是一个例子:

        public int Add(int x, int y)
        {
            return x + y;

            int GetX()
            {
                return x;
            }

            static int GetY()
            {
                return y;//Compiler error:A static local function cannot contain a reference to 'y'
            }
        }

上面的例子中,Add函数包含两个local函数,其中GetY是static的,但是它引用了外围的本地变量y,所以会有注释中的编译错误。

Disposable ref structs

因为ref的结构体不能实现任何接口,当然IDisposable也不行,所以它就不能被dispose了。在C# 8.0中,只要它有一个可访问的void Dispose()函数,就可以被Dispose,例子如下:

    ref struct DisposableRefStruct
    {
        public void Dispose()
        {
            Console.WriteLine("ref struct dispose method is called");
        }
    }

    public class DisposableRefStructTester
    {
        public static void Test()
        {
            using (var s = new DisposableRefStruct())
            {
                Console.WriteLine("Test Disposable Ref Struct");
            }
        }

从例子中可以看出DisposableRefStruct没有实现IDisposable接口,但是有void Dispose()方法,所以也可以用字using语句中。该功能同样适用于readonly ref struct

可空引用类型(Nullable reference types)

可以通过在工程文件中加上下面的配置来打开可空引用类型检查。打开之后,编译器会认为所有的引用类型都是不可以赋空值的,必须像之前的可空值类型一样,在类型后面加上?来表示这个引用类型可以为空。具体细节请参考:
https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references
https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/nullable-reference-types
https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/upgrade-to-nullable-references

<Nullable>enable</Nullable>

异步流(Asynchronous streams)

该功能是对应于同步IEnumerable的异步版本,异步流函数具有下面三个特点:

  1. 使用async修饰
  2. 返回类型为:IAsyncEnumerable
  3. 函数包含yield return语句来实现enumerator范式
    下面是一个例子:
        public static async IAsyncEnumerable<int> GenerateSequence()
        {
            for (int i = 0; i < 20; i++)
            {
                await Task.Delay(100);
                yield return i;
            }
        }

        public static async Task ConsumeAsyncStream()
        {
            await foreach (var i in GenerateSequence())
            {
                Console.WriteLine(i);
            }
        }

上例中GenerateSequence返回一个异步流而ConsumeAsyncStream通过在foreach前面加一await来枚举该异步流。

索引和范围(Indices and ranges)

索引和范围为访问序列中的单个元素或范围提供了简洁的语法。这个概念有点像Python中的切片,只是语法稍有不同。该功能的实现依赖于以下的两个新类型和两个新操作符:

  1. System.Index,表示序列中的一个索引
  2. ^操作符,表示索引是从后面开始的
  3. System.Range,表示序列的一个子序列
  4. ...操作符,表示一个range的开始和结束

    一些代码示例如下:

public class IndicesAndRanges
    {
        public string[] 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

        public void Test()
        {
            Console.WriteLine($"The last word is {words[^1]}"); // writes "dog"
            var quickBrownFox = words[1..4];// "quick", "brown", and "fox"
            var lazyDog = words[^2..^0];//"lazy" and "dog"

            var allWords = words[..]; // contains "The" through "dog".
            var firstPhrase = words[..4]; // contains "The" through "fox"
            var lastPhrase = words[6..]; // contains "the", "lazy" and "dog"

            Range phrase = 1..4;//Range phrase = 1..4;
            var text = words[phrase];
        }
    }

Null合并赋值

C# 8.0引入了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#8.0开始,如果构造的值类型仅包含非托管类型的字段,则该类型不受管理,例子如下:

public struct Coords<T>
{
    public T X;
    public T Y;
}

它和其他非托管类型一样,可以创建指向它的指针或在stack上分配该类型的内存块。

嵌套表达式中的 stackalloc

C# 8.0中,如果stackalloc表达式结果的类型为System.Span或者System.ReadOnlySpan,则可以在其他表达式中使用该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

字符串插值的增强

字符串插值中$和@标记的顺序可以任意放置:[email protected]"..." 和 @$"..."都是正确的,而在早期的 C# 版本中,$标记必须出现在@标记之前。

原文地址:https://www.cnblogs.com/dereklovecc/p/12393155.html

时间: 2024-10-15 12:26:00

C# 8.0中的新功能的相关文章

AndroidStudio3.0 下载使用新功能介绍

"-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> AndroidStudio3.0 下载使用新功能介绍 - 天平 - 博客频道 - CSDN.NET 天平 向着目标一步一步走. 目录视图 摘要视图 订阅 [活动]2017 CSDN博客专栏评选 &nbsp [5月书讯]流畅的Python,终于等到你!    &

Android Studio 3.0 下载 使用新功能介绍

谷歌2017发布会更新了挺多内容的,而且也发布了AndroidStudio3.0预览版,一些功能先睹为快.(英语一般,有些翻译不太好) 下载地址 https://developer.android.google.cn/studio/archive.html 选择显示全部即可看到下载地址,这里给出来了. Windows (64-bit): android-studio-ide-171.4010489-windows.zip (702075896 bytes) https://dl.google.c

在C#6.0中的新语言特性

孙广东    2015.6.5 What's New in C# 6: http://channel9.msdn.com/Shows/Visual-Studio-Toolbox/Whats-New-in-C-6 Cross Platform Development系列:http://channel9.msdn.com/Shows/CZSK-videa/Cross-Platform-Development-1-Introduction Developer Productivity: What's

Unity 4.0 中的新动画系统——MecAnim

分享一个文档资料,关于动画系统的,版本应该很老了,但是有借鉴意义的: Unity 4.0 已于 2012 年 11 月 15 日正式发布,Unity 每一次版本的提升,都给游戏开发者带来惊喜,这一次也不例外,该版本最大变化之一就是引入了新动画系统--MecAnim,通过 MecAnim 新的动画系统,开发者在 Unity 中可以非常方便地实现角色动画.所以 Unity 4.0 的启动画面都突出了新的动画系统这一最新功能  . 这里需要说明的是,在 Unity 4.0 中,开发者还是可以使用原有的

(数据科学学习手札73)盘点pandas 1.0.0中的新特性

本文对应脚本及数据已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 毫无疑问pandas已经成为基于Python的数据分析领域最重要的包,而就在最近,pandas终于迎来了1.0.0版本,对于pandas来说这是一次更新是里程碑式的,删除了很多旧版本中臃肿的功能,新增了一些崭新的特性,更加专注于高效实用的数据分析,本文就将针对pandas 1.0.0在笔者眼中比较重要的特性进行介绍,对于想要完整彻底了解新版

SQLSERVER2014中的新功能

SQLSERVER2014中的新功能 转载自:http://blog.csdn.net/maco_wang/article/details/22701087 博客人物:maco_wang SQLSERVER方向MVP.MSDN论坛版主 红色标记的为本人认为的重点功能 1.内存优化表    内存中 OLTP 是一种内存优化的数据库引擎,它集成到 SQL Server 引擎中.    内存中 OLTP 已针对 OLTP 进行优化.对于内存中 OLTP 支持的 Transact-SQL 外围应用存在很

Kafka 0.11版本新功能介绍 —— 空消费组延时rebalance

在0.11之前的版本中,多个consumer实例加入到一个空消费组将导致多次的rebalance,这是由于每个consumer instance启动的时间不可控,很有可能超出coordinator确定的rebalance timeout(即max.poll.interval.ms),而每次rebalance的代价又相当地大,因为很多状态都需要在rebalance前被持久化,而在rebalance后被重新初始化.曾经有个国外用户,他有100个consumer,每次rebalance的时间甚至要1个

C# 7.0 中的新增功能

来源:Mark Michaelis 链接:msdn.microsoft.com/magazine/mt790184   解构函数 从 C# 1.0 开始,就能调用函数,就是将参数组合起来并封装到一个类中的构造函数.但是,从来没有一种简便的方式可将对象解构回其各个组成部分.例如,假设有一个 PathInfo 类,它采用文件名的每个元素(目录名.文件名.扩展名),并将它们组合成一个对象,然后支持操作对象的不同元素.现在,假设你需要将该对象提取(解构)回其各个组成部分. 在 C# 7.0 中,通过解构

Apache Flink 1.3.0正式发布及其新功能介绍

下面文档是今天早上翻译的,因为要上班,时间比较仓促,有些部分没有翻译,请见谅. 2017年06月01日儿童节 Apache Flink 社区正式发布了 1.3.0 版本.此版本经历了四个月的开发,共解决了680个issues.Apache Flink 1.3.0 是 1.x.y 版本线上的第四个主要版本,其 API 和其他 1.x.y 使用 @Public 注释的API是兼容的. 此外,Apache Flink 社区目前制定了每四月发行一个主要版本(Apache Flink 1.2.0 是201