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的一些主要变更都总结完成了,有一些特性变更不是很方便整合到一个实例中去,但又比较重要,就单拉出来说了

  • 相关阅读:
    iOS应用程序的登录界面
    访问Mac下virtualbox中的win8.1虚拟机
    JASIG-CAS学习笔记——初探CAS
    跨域读取Cookies(续)
    跨域读取Cookies
    错误——无法找到com/* /* /**.xml
    设计模式学习之——简单工厂、工厂方法、抽象工厂方法
    spring+ibatis+dwr+ext项目整合
    SenchaTouch学习——form表单
    FLEX自定义事件
  • 原文地址:https://www.cnblogs.com/TanSea/p/CSharp-History-Feature.html
Copyright © 2011-2022 走看看