zoukankan      html  css  js  c++  java
  • C# 历史版本特性变更

    官方的C#历史版本特性变更是按版本排序的,知识点有些乱,我就产生了一个想法,以一个数据库操作模块对这些知识做一个整合

    注意事项:

    1、不一定会整合所有的特性变更,但是努力整合

    2、由于目的是在介绍历史版本特性,代码的实现不一定是最优方案

    废话不多说,先上一段实体类代码

    定义实体类

    public class BaseEntity
    {
        private Guid _id;
        public Guid Id { get { return _id; } set { _id = value; } }
        public DateTime CreateTime { get; set; }
        public Nullable<DateTime> UpdateTime { get; set; }
        public bool IsDelete { get; set; } = false;
    }
    public partial class Member : BaseEntity
    {
        private string _firstName;
        public string FirstName
        {
            get => _firstName;
            set => _firstName = (!string.IsNullOrWhiteSpace(value)) ? value : throw new ArgumentException("FirstName不能为空");
        }
        public string LastName { get; set; }
        public DateTime? BirthOfDay { get; set; }
    }
    public partial class Member
    {
        public string FullName => $"{FirstName} {LastName}";
        public int? Age { get; init; }
        public override string ToString() => $"{FirstName} {LastName}"; 
    }

    分部类型(Partial types)【C# 2.0】

    拆分一个类、一个结构、一个接口或一个方法的定义到两个或更多的文件中

    public partial class Member : BaseEntity
    {
        private string _firstName;
        public string FirstName
        {
            get => _firstName;
            set => _firstName = value;
        }
        public string LastName { get; set; }
        public DateTime? BirthOfDay { get; set; }
    }
    public partial class Member
    {
        public string FullName => $"{FirstName} {LastName}";
        public int? Age { get; init; }
        public override string ToString() => $"{FirstName} {LastName}";
    }

    以上实体类的定义,BaseEntity类用来定义数据库表的公共字段,Member类用partial定义成分部类。个人习惯一个分部定义映射属性,一个分部定义扩展属性

    可为空的值类型(Nullable value types)【C# 2.0】

    建立数据映射时,有些属性可能是没有数据的,例如:UpdateTime、BirthOfDay

    public Nullable<DateTime> UpdateTime { get; set; }
    public DateTime? BirthOfDay { get; set; }

    Nullable<T>用来定义可空值类型,也可以简写成T?,T为不能为空的值类型。官方推荐使用T?

    既然提到了可空类型,那么就把相关的变更也一起说一下

    Null传播器(Null propagator)【C# 6.0】

    从字面上理解,就是将Null传播到下一级属性

    Guid id = member.Id; // id是Guid类型,但member为空时会报异常
    Guid? id = member?.Id; // id是Guid?类型,member可以为空;当member为空时,id也为空;当member不为空时,id = member.Id

    Null传播器的出现,使得我们不需要写大量的代码来判断对象是否为空

    默认文本表达式(Default literal expressions)【C# 7.1】

    在大多数情况下,我们并不喜欢对可空类型进行操作,更希望给他一个默认值

    过去,可以这样给一个变量赋默认值

    Guid id = member?.Id ?? default(Guid);

    现在,可以把后面的类型省略掉

    Guid id = member?.Id ?? default;

    Null合并赋值(Null-coalescing assignment)【C# 8.0】

    member ??= new Member();

    当member为空时,实例化,上面的代码和下面的代码意思是一样的

    if (member == null)
        member = new Member();

    自动实现的属性(Auto-Implemented properties)【C# 3.0】

    过去,定义属性代码量有些多

    private Guid _id;
    public Guid Id
    {
        get { return _id; }
        set { _id = value; }
    }

    现在,一行代码就可以了

    public DateTime CreateTime { get; set; }

    自动属性初始化表达式(Auto property initializers)【C# 6.0】

    过去,给属性初始化需要在构造方法中实现,现在,实现属性的时候可以直接初始化了

    public bool IsDelete { get; set; } = false;

    表达式主体定义(Expression body definition)【C# 6.0 - 7.0】

    表达式主体使用 member => expression 来定义,在C# 6.0支持方法、运算符和只读属性,在C# 7.0支持构造函数、终结器、属性和索引器访问器

    public override string ToString() => $"{FirstName} {LastName}"; // 方法
    public string FullName => $"{FirstName} {LastName}"; // 属性

    字符串内插(string interpolation)【C# 6.0】

    过去,字符串的拼接是这样的

    string fullName = string.Format("{0} {1}", firstName, lastName);

    现在,使用$将变量直接写到花括号内

    string fullName = $"{firstName} {lastName}";

    仅限 Init 的资源库(Init only setters)【C# 9.0】

    init访问器定义的属性仅在构造时可以赋值,实例化之后属性就变成只读了

    public int? Age { get; init; }

    年龄属性是根据生日属性计算出来了,适合使用init访问器

    添加记录

    public bool Insert(Member member)
    {
        return _dbHelper.Insert(member);
    }

    一个简单的Insert方法,通过数据库帮助类把实体类添加进数据库。这里就有一个问题,数据库表肯定不只一个,那么我们就需要定义很多这些类似的方法。不管是手撸还是用生成器代码量还是很多

    泛型(Generics)和泛型约束(Constraints)【C# 2.0】

    延时指定类型,也就是在调用方法时再指定参数的类型。泛型的命名通常是以大写T开头

    public bool Insert<TEntity>(TEntity entity)
    {
        return _dbHelper.Insert<TEntity>(entity);
    }

    泛型可以指定任何类型,但在这里我们要求只能是实体类,所以这里要用到泛型约束

    public bool Insert<TEntity>(TEntity entity) where TEntity: class 
    {
        return _dbHelper.Insert<TEntity>(entity);
    }

    用where来指定泛型只能是类,但是这仍然还不够,我们要求的是和数据库有映射关系的实体类,所以要对泛型进一步约束

    public bool Insert<TEntity>(TEntity entity) where TEntity: BaseEntity
    {
        return _dbHelper.Insert<TEntity>(entity);
    }

    这里指定泛型只能是BaseEntity及继承自BaseEntity的类

    对象和集合初始值设定项(Object and collection initializers)【C# 3.0】

    过去,初始化一个实体类要么在构造方法中实现,要么先实例化之后再一个个的赋值

    Member member = new Member();
    member.Id = Guid.NewGuid();
    member.FirstName = "Tan";
    member.LastName = "Sea";
    member.CreateTime = DateTime.Now;
    
    bool isSuccess = Insert<Member>(member);

    现在,可以用以下的方式来初始化

    Member member = new Member
    {
        Id = Guid.NewGuid(),
        FirstName = "Tan",
        LastName = "Sea",
        CreateTime = DateTime.Now
    };
    bool isSuccess = Insert<Member>(member);

    目标类型的 new 表达式(Target-typed new expressions)【C# 9.0】

    过去,实例化类型时 new 后面是要接类型的

    Member member = new Member();

    现在,因为实例化的时候已经确定了类型,在后面就不用再接类型了

    Member member = new();

    异步方法(async/await)【C# 5.0】

    异步方法可以避免 UI线程卡顿,提高系统吞吐率

    过去,用Thread来实现异步方法,Thread功能很强大但相对的非常难用好。后来ThreadPool针对Thread进行了一次封装,只要将线程提交给它即可,其他的什么也做不了

    现在,用async/await定义异步方法,异步方法命名通常用Async结尾

    public async Task<bool> InsertAsync<TEntity>(TEntity entity) where TEntity: BaseEntity
    {
        return await _dbHelper.InsertAsync<TEntity>(entity);
    }

    async/await的本质是状态机,如果想了解更多可以查看官方文档

    查询记录

    public async Task<IEnumerable<TEntity>> GetEntitiesAsync<TEntity>(Expression<Func<TEntity, bool>> whereExpression) where TEntity: BaseEntity
    {
        return await _dbHelper.GetEntitiesAsync<TEntity>(whereExpression);
    }
    IEnumerable<Member> adultMembers = await GetEntitiesAsync<Member>(member => member.Age >= 18 && member.IsDelete == false);

    建议在判断布尔值为假时,使用 member.IsDelete == false 而不是 !member.IsDelete,前者的可读性更高一些

    匿名方法(Anonymous methods)【C# 2.0】

    Func<Member, bool> func = delegate (Member member) { return member.Age >= 18 && member.IsDelete == false; };

    拉姆达表达式(Lambda expressions)【C# 3.0】

    在C# 3.0版本之后,都是用拉姆达表达式来定义匿名方法了,拉姆达表达式格式分两种:

        (input-parameters) => expression

    Func<Member, bool> func = member => member.Age >= 18 && member.IsDelete == false;

        (input-parameters) => { <sequence-of-statements> }

    Func<Member, bool> func = member => { return member.Age >= 18 && member.IsDelete == false; };

    本地函数(Local functions)【C# 7.0】

    说到匿名方法了,可以再说一下本地函数。在方法内定义,和匿名方法不同的是,本地函数可以定义在调用之后

    public void Method()
    {
        LocalMethod(member);
        bool LocalMethod(Member member)
        {
            return member.Age >= 18 && member.IsDelete == false;
        }
    }

    静态本地函数(Static local functions)【C# 8.0】

    静态本地函数和本地函数的作用域都是一样的,区别在于静态本地函数不访问封闭范围中的任何变量

    public void Method()
    {
        int age = 18;
        LocalMethod(member);
        static bool LocalMethod(Member member)
        {
            return member.Age >= age && member.IsDelete == false; // 错误,这里使用了封闭范围中的变量
        }
    }

    表达式树(Expression Trees)【C# 3.0】

    使用表达式类构造一段代码,再通过对这段代码的解释来完成特定的需求。微软的Entity Framework就是将表达式树解释成SQL语句来操作数据库的

    Expression<Func<Member, bool>> expr = member => member.Age >= 18 && member.IsDelete == false;

    那么怎么把查询传入的参数构造成一段表达式代码并解释成SQL语句,这个要看更深入的了解表达式树,这里就不再展开了

    查询分页记录

    public IEnumerable<TEntity> GetEntitiesPage<TEntity>(Expression<Func<TEntity, bool>> whereExpression,
        out int totalRecord, int pageIndex = 1, int pageSize = 10) where TEntity : BaseEntity
    {
        return _dbHelper.GetEntitiesPage<TEntity>(whereExpression, out totalRecord, pageIndex, pageSize);
    }
    int totalRecord = 0;
    IEnumerable<Member> members = GetEntitiesPage<Member>(member => member.IsDelete == false, out totalRecord, 2, 20);
    var result = new { Members = members, TotalRecord = totalRecord };

    可以看出来这个方法的定义没有使用异步方法,因为异步方法不能使用out关键字

    out变量(out variables)【C# 7.0】

    现在,使用out变量的时候不用先定义了,可以直接在out变量后面定义

    IEnumerable<Member> members = GetEntitiesPage<Member>(member => member.IsDelete == false, out int totalRecord, 2, 20);

    命名参数和可选参数(Named and optional arguments)【C# 4.0】

    在定义方法时,在pageIndex和pageSize之后添加了一个默认值,在调用方法时,可以不用传参

    IEnumerable<Member> members = GetEntitiesPage<Member>(member => member.IsDelete == false, out totalRecord);

    也可以给指定参数赋值

    IEnumerable<Member> members = GetEntitiesPage<Member>(member => member.IsDelete == false, out totalRecord, pageSize: 20);

    隐式类型本地变量(Implicitly typed local variables)【C# 3.0】

    使用var来在方法内定义隐式类型,隐式类型也是属于强类型,编译器会根据初始化时的值来决定是什么类型

    var i = 5; // i is int
    var str = "name" // str is string

    动态类型(Dynamic Type)【C# 4.0】

    dynamic i = 5;
    dynamic str = "name"

    dynamic和var一样都是定义隐式类型。不同的是var是属于强类型,而dynamic是弱类型,它会绕过编译时类型检查

    dynamic和object的行为类似,任何非空的表达式都可以转换为dynamic

    匿名类型(Anonymous Types)【C# 3.0】

    通常,我们不需要返回实体类的所有属性,可以定义一个匿名类型

    var result = new { Members = members, TotalRecord = totalRecord };

    在new后面不指定类型就是匿名类型,由于类型无法确定,result变量只能定义成隐式类型

    var是配合匿名类型一起使用的,在可以显式的定义类型时不推荐使用var

    查询表达式(Query expressions)【C# 3.0】

    一种和SQL很像的表达式,现在用得很少了,简单的介绍一下,如果要深入了解可以去查看官方文档

    var result = from m in members
            select new { Members = members, TotalRecord = totalRecord };

    元组(Tuples)【C# 7.0】

    要使用异步方法,就不能用out参数来返回值,定义成对象返回结果是一种解决方案

    public async Task<Response<TEntity>> GetEntitiesPageAsync<TEntity>(Expression<Func<TEntity, bool>> whereExpression,
        int pageIndex = 1, int pageSize = 10) where TEntity : BaseEntity
    {
        return await _dbHelper.GetEntitiesPageAsync<TEntity>(whereExpression, pageIndex, pageSize);
    }
    
    public class Response<T>
    {
        public IEnumerable<T> Entities { get; set; }
        public int TotalRecord { get; set; }
    }
    Response<Member> response = await GetEntitiesPageAsync<Member>(member => member.IsDelete == false, 2, 20);

    而另一种解决方案就是使用元组来返回多个值

    public async Task<(IEnumerable<TEntity>, int)> GetEntitiesPageAsync<TEntity>(Expression<Func<TEntity, bool>> whereExpression,
        int pageIndex = 1, int pageSize = 10) where TEntity : BaseEntity
    {
        return await _dbHelper.GetEntitiesPageAsync<TEntity>(whereExpression, pageIndex, pageSize);
    }
    (IEnumerable<Member> members, int totalRecord) = await GetEntitiesPageAsync<Member>(member => member.IsDelete == false, 2, 20);

    弃元(Discards)【C# 7.0】

    当然,有时候可能不需要元组中的某一些参数

    (IEnumerable<Member> members, _) = await GetEntitiesPageAsync<Member>(member => member.IsDelete == false, 2, 20);

    下划线变量是一个只写不读的变量,同样的微软也推荐在任何不需要返回值的时候显式的用弃元

    _ = Insert<Member>(member);

    元组解析(Deconstruction)【C# 7.0】

    public void Deconstruct(out string firstName, out string lastName) => (firstName, lastName) = (FirstName, LastName);

    在类中定义了Deconstruct方法之后,我们可以用元组来提取类中的各个字段

    Member member = new Member
    {
        Id = Guid.NewGuid(),
        FirstName = "Tan",
        LastName = "Sea"
    };
    (string FirstName, string LastName) = member;

     修改实体类

    现在,业务拓展了,需要记录会员的消费次数和最后的消费时间,通过这2个属性来得到会员的状态

    public partial class Member : BaseEntity
    {
        // 代码略
        public int ConsumeTimes { get; set; }
        public DateTime LastConsumeTime { get; set; }
    }
    public partial class Member
    {
        // 代码略
        public string ConsumeState { get; set; }
    }
    public IEnumerable<Member> GetMembersWithConsumeState()
    {
        IEnumerable<Member> members = _dbHelper.GetEntities<Member>(member => member.IsDelete == false);
        foreach (var member in members)
        {
            member.ConsumeState = member.GetConsumeState();
            yield return member;
        }
    }

    扩展方法(Extension methods)【C# 3.0】

    给已知类型添加新的方法,GetConsumeState就是给Member类型添加的扩展方法

    public static class ExtensionMethod
    {
        public static string GetConsumeState(this Member member)
        {
            var days = DateTime.Now.Subtract(member.LastConsumeTime).Days;
            if (member.ConsumeTimes == 1)
            {
                return "新客户";
            }
            else if (member.ConsumeTimes >= 10)
            {
                return "老客户";
            }
            else
            {
                if (days <= 30)
                {
                    return "活跃客户";
                }
                else if (days <= 60)
                {
                    return "非活跃客户";
                }
                else
                {
                    return "静默客户";
                }
            }
        }
    }

    扩展方法的类和方法都要是静态方法,this就是给哪个类型添加方法

    模式匹配(Pattern matching)【C# 7.0 - 9.0】

    匹配一个类型,如果成功给变量赋值,如果不成功变量为默认值

    模式匹配从7.0开始,每个版本都有增强,7.0支持is和switch。8.0支持switch表达式、属性模式、元组模式、位置模式。9.0支持and、or、not模式

    简单的is模式匹配,当input是int时,赋值给count并参与求和。当input不是int时,count赋默认值。

    var input = "你好";
    var sum = 0;
    if (input is int count)
        sum += count;

    元组模式匹配,重写上面的GetConsumeState方法

    public static string GetConsumeState(this Member member)
    {
        var days = DateTime.Now.Subtract(member.LastConsumeTime).Days;
        return (member.ConsumeTimes, days) switch
        {
            (1, _) => "新客户",
            ( >= 10, _) => "老客户",
            ( > 1 and < 10, <= 30) => "活跃客户",
            ( > 1 and < 10, <= 60) => "非活跃客户",
            _ => "静默客户"
        };
    }

    更多的模式匹配用法请查看官方文档,这里就不再介绍了

    迭代器(Iterators)【C# 2.0】

    当返回类型为IEnumerable、IEnumerable<T>、IEnumerator 或 IEnumerator<T>时,可以使用yield return来逐个返回元素。迭代器会保留状态并在下次进入方法时继续执行

    public IEnumerable<Member> GetMembersWithConsumeState()
    {
        IEnumerable<Member> members = _dbHelper.GetEntities<Member>(member => member.IsDelete == false);
        foreach (var member in members)
        {
            member.ConsumeState = member.GetConsumeState();
            yield return member;
        }
    }

    如果members里面有10个元素,则yield return会返回10次,每次返回1个元素

    异步流(Asynchronous streams)【C# 8.0】

    迭代器的异步版本,可以看出来这中间经过了很多版本,直到出了IAsyncEnumerable才解决了迭代器异步的问题

    public async IAsyncEnumerable<Member> GetMembersWithConsumeStateAsync()
    {
        IEnumerable<Member> members = await _dbHelper.GetEntitiesAsync<Member>(member => member.IsDelete == false);
        foreach (var member in members)
        {
            member.ConsumeState = member.GetConsumeState();
            yield return member;
        }
    }

    可以看出来,这个异步流的写法和之前异步方法Task<IEnumerable<Member>>不一样

    其他变更

    默认接口方法(Default interface methods)【C# 8.0】

    现在可以将成员添加到接口,并为这些成员提供实现

    void Main()
    {
        ILogger foo = new Logger();
        foo.Log (new Exception ("test")); 
    }
    
    class Logger : ILogger
    { 
        public void Log (string message) => Console.WriteLine (message);
    }
    
    interface ILogger
    {
        void Log (string message); 
    
        // Adding a new member to an interface need not break implementors:
        public void Log (Exception ex) => Log (ExceptionHeader + ex.Message);
    
        // The static modifier (and other modifiers) are now allowed:
        static string ExceptionHeader = "Exception: ";
    }

    静态引用(Using Static)【C# 6.0】

    过去,我们只能对命名空间进行引用

    using System;
    
    Math.Round(3.1415926); // Math是静态类

    现在,我们可以对静态的类进行引用

    using static System.Math;
    
    Round(3.1415926);

    using 声明(Using declarations)【C# 8.0】

    过去,我们对继承了IDisposable接口的对象是这么使用的,代码会在花括号结束时自动释放资源

    using (var file = new System.IO.StreamWriter("WriteLines.txt"))
    {
        代码略
    }

    现在,使用using关键字声明的变量在封闭范围的末尾释放资源,两个用法差别不大(可以脑补一下他们的区别),但是代码的整洁度后者更好

    using var file = new System.IO.StreamWriter("WriteLines.txt");

    nameof 表达式(nameof operator)【C# 6.0】

    nameof 表达式可生成变量、类型或成员的名称作为字符串常量

    Console.WriteLine(nameof(System.Collections.Generic));  // output: Generic
    Console.WriteLine(nameof(List<int>));  // output: List
    Console.WriteLine(nameof(List<int>.Count));  // output: Count
    Console.WriteLine(nameof(List<int>.Add));  // output: Add
    
    var numbers = new List<int> { 1, 2, 3 };
    Console.WriteLine(nameof(numbers));  // output: numbers
    Console.WriteLine(nameof(numbers.Count));  // output: Count
    Console.WriteLine(nameof(numbers.Add));  // output: Add

    异常筛选器(Exception filters)【C# 6.0】

    对指定条件进行catch,其他条件不catch

    try
    {
        ......
    }
    catch (Exception e) when (e.Message.Contains("404"))
    {
        return;
    }

    索引和范围(Indices and ranges)

    类似于Python的切片,例子很清晰,就不多解释了

    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
    var quickBrownFox = words[1..4]; // 从索引1到索引4,但不包括索引4
    var lazyDog = words[^2..^0]; // 从索引^2到索引^0,但不包括索引^0
    var allWords = words[..]; // 从开始到结束,从The到dog
    var firstPhrase = words[..4]; // 从开始到索引4,但不包括索引4。从The到fox
    var lastPhrase = words[6..]; // 从索引6到结束,从the到dog

    数字文本语法改进(Numeric literal syntax improvements)【C# 7.0】

    增加数字的可读性,给数字添加分隔符

    public const int Sixteen =   0b0001_0000;
    public const int ThirtyTwo = 0b0010_0000;
    public const int SixtyFour = 0b0100_0000;
    public const int OneHundredTwentyEight = 0b1000_0000;
    
    public const long BillionsAndBillions = 100_000_000_000;
    
    public const double AvogadroConstant = 6.022_140_857_747_474e23; // 阿伏伽德罗常量
    public const decimal GoldenRatio = 1.618_033_988_749_894_848_204_586_834_365_638_117_720_309_179M; // 黄金比例

    调用方信息(Determine caller information)【C# 5.0】

    可以获取调用方的一些信息方便调试,比如方法名,方法所在源文件路径,方法所在源文件的行数等

    public void TraceMessage(string message,
        [CallerMemberName] string memberName = "",
        [CallerFilePath] string sourceFilePath = "",
        [CallerLineNumber] int sourceLineNumber = 0)
    {
        System.Diagnostics.Trace.WriteLine("message:" + message);
        System.Diagnostics.Trace.WriteLine("member name:" + memberName);
        System.Diagnostics.Trace.WriteLine("source file path:" + sourceFilePath);
        System.Diagnostics.Trace.WriteLine("source line number:" + sourceLineNumber);
    }

    结语

    C# 2.0到9.0的一些主要变更都总结完成了,有一些特性变更不是很方便整合到一个实例中去,但又比较重要,就单拉出来说了

  • 相关阅读:
    Java实现 LeetCode 69 x的平方根
    Java实现 LeetCode 68 文本左右对齐
    Java实现 LeetCode 68 文本左右对齐
    Java实现 LeetCode 68 文本左右对齐
    Java实现 LeetCode 67 二进制求和
    Java实现 LeetCode 67 二进制求和
    Java实现 LeetCode 67 二进制求和
    Java实现 LeetCode 66 加一
    Java实现 LeetCode 66 加一
    CxSkinButton按钮皮肤类
  • 原文地址:https://www.cnblogs.com/TanSea/p/CSharp-History-Feature.html
Copyright © 2011-2022 走看看