zoukankan      html  css  js  c++  java
  • [C#]6.0新特性浅谈

    C#6.0出来也有很长一段时间了,虽然新的特性和语法趋于稳定,但是对于大多数程序猿来说,想在工作中用上C#6.0估计还得等上不短的一段时间。
    所以现在再来聊一聊新版本带来的新特性可能也还不算晚吧?

    一、nameof关键字

    这绝对是整个新版本最让我期待的内容,它给代码重构带来了巨大的便利。
    先来看一下它是怎么使用的吧:

    string s;
    Console.WriteLine(nameof(s));
    s = nameof(s.Length);
    Console.WriteLine(nameof(String));
    Console.WriteLine(nameof(string.Length));
    Console.WriteLine(nameof(string.Substring));

    运行结果:

    s
    String
    Length SubString

    通过上面的示例,可以看出来以下几点:

    1.它不在乎变量是否已经初始化
    2.它构成了一个运算结果为字符串的(编译时)表达式
    3.它可以用于取得类型名,但是nameof(string)是不能通过编译的,小写的string是关键字而不是类型(这一点很值得吐槽。。。)
    4.它的括号里面可以直接从类型取得实例属性
    5.它可以取得方法名

    然后看这段代码的IL:

    IL_0000: ldstr "s"
    IL_0005: call void [mscorlib]System.Console::WriteLine(string)
    IL_000a: ldstr "String"
    IL_000f: call void [mscorlib]System.Console::WriteLine(string)
    IL_0014: ldstr "Length"
    IL_0019: call void [mscorlib]System.Console::WriteLine(string)
    IL_001e: ldstr "Substring"
    IL_0023: call void [mscorlib]System.Console::WriteLine(string)
    IL_0028: ret

    编译后看不到nameof的痕迹,编译器把nameof的运算结果硬编码了,所以说它是一个"编译时运算符"。
    适用场景:

    1.空引用异常信息构成
    2.ToString方法
    3.IList数据绑定的列名

    主要吐槽一下第三条吧,这是我最近工作里遇到的很闹心的一个事情,什么时候用上了6.0就能彻底解决这个麻烦了。。。
    想象一下以前绑定一个自定义类型的List到ListBox吧,要设定DisplayMember和ValueMember的话就只能是硬编码,像是这样:

    listBox1.DisplayMember = "ID";
    listBox1.ValueMember = "Content";

    一旦要对这个绑定类型的属性名称进行更改,工作量简直不敢想象。。。好一点的做法是用一套常量来代替硬编码,但是这样带来的麻烦是还得记着常量名。
    不过以后用上了nameof就爽快了,一个Ctrl+R,R通通搞定~

    二、[.?]空引用判断操作符

    这算是一个用于简洁代码的语法糖吧,个人觉得实用价值一般般。
    先看怎么用的吧:

    string s = null;
    s = s?.Substring(1);
    // string expr_07 = this.s;
    // this.s = ((expr_07 != null) ? expr_07.Substring(0) : null);
    Console.WriteLine(s == null);

    第二行代码与第三行被注释掉的部分,在编译过后是完全相等的。
    同时也就是说一旦用了[.?],返回值就有可能是null,所以对于原本返回值类型的成员,只能赋值给Nullable<?>了,比如这样:

    string s = null;
    int? i = s?.IndexOf(".");
    int j = s.IndexOf(".");

    至于之后再要用到变量i,很多情况下仍然需要对是否空值进行判断。。。
    同时这个语法糖也带来了歧义,比如这样:

    object tag = form?.Tag;

    由于Form和Tag都是引用类型,都可能为null,如果变量tag是null,这时候是没办法知道到底是form还是Tag返回了null(除非再判断一次。。。)。

    三、字符串嵌入值

    同样是一个用于简洁代码的语法糖,先看怎么用吧:

    int i = 1;
    Console.WriteLine($"{nameof(i)} + 1 = {i + 1}");
    Console.WriteLine($"{i + 1} * {i + 1} = 4");

    运行结果:

    i + 1 = 2
    2 * 2 = 4

    然后是IL:

    IL_0000: ldc.i4.1
    IL_0001: stloc.0
    IL_0002: ldstr "{0} + 1 = {1}"
    IL_0007: ldstr "i"
    IL_000c: ldloc.0
    IL_000d: ldc.i4.1
    IL_000e: add
    IL_000f: box [mscorlib]System.Int32
    IL_0014: call string [mscorlib]System.String::Format(string, object, object)
    IL_0019: call void [mscorlib]System.Console::WriteLine(string)
    IL_001e: ldstr "{0} * {1} = 4"
    IL_0023: ldloc.0
    IL_0024: ldc.i4.1
    IL_0025: add
    IL_0026: box [mscorlib]System.Int32
    IL_002b: ldloc.0
    IL_002c: ldc.i4.1
    IL_002d: add
    IL_002e: box [mscorlib]System.Int32
    IL_0033: call string [mscorlib]System.String::Format(string, object, object)
    IL_0038: call void [mscorlib]System.Console::WriteLine(string)
    IL_003d: ret

    可以看出来以下几点:

    1.大括号可以用于包裹表达式
    2.相同的表达式需要计算两次

    介于第二条,对于资源消耗较多的运算,还是用一个中间变量放到$字符串中更好,要么直接使用String.Format。
    同时需要注意的是,$和@同时使用的时候必须把$写在@之前,而在正则表达式中的大括号中的内容会被优先当做C#表达式计算一遍,比如:

    Regex.IsMatch("AAA", $@"A{3}");
    Regex.IsMatch("AAA", String.Format("A{0}", 3))

    上下两行的编译结果是一样的,然而这样的编译结果显然不是我们想要的,所以我建议在正则表达式上不要使用字符串嵌入值。

    四、lambda方法体

    仍然是用于简洁代码的特性,如下:

    private void LambdaMethod() => Console.WriteLine(nameof(LambdaMethod));
    private string LambdaProperty => nameof(LambdaProperty);

    任何用一句话就能搞定的方法从此都可以扔掉大括号和return关键字了。注意第二行的内容,能且仅能实现属性的get方法,所以这构成了一个只读属性。
    上面这两行内容其实就是相当于这样的:

    private void LambdaMethod()
    {
        Console.WriteLine("LambdaMethod");
    }
    
    private string LambdaProperty
    {
        get
        {
            return "LambdaProperty";
        }
    }

    在以前的版本我也可能这么写:

    private Action LambdaMethod = () => Console.WriteLine(nameof(LambdaMethod));

    这种写法对于方法还好说,属性想要这么写就不行了。。。当然,这种写法总的来说是不可取的。

    五、属性初始化器

    这个特性算是盼星星盼月亮终于盼来了,虽然说重要性可能不是那么大,但是以前版本的C#居然不这么设计着实让我有些难以理解。。。
    用法就像是在字段前加get set器,在属性后加赋值:

    private string InitedProperty { get; set; } = "InitedProperty";

    和上一条特性中的lambda属性看起来有点像,但是其实是有很大不同的:

    1.带属性初始化器的属性就和自动set get器属性一样,是有自动生成的字段的;而lambda属性是不会自动生成私有字段的
    2.属性初始化器的等号后只能是静态成员;而实例lambda属性中可以是任何表达式
    3.属性初始化器等号后的表达式只会在类型加载时运算一次;而lambda属性的表达式会在每一次调用属性时即时运算
    4.属性初始化器不影响属性可写性;而lambda属性就只能读了

    基于以上第三条,如果初始化表达式耗费资源较多,应该使用属性初始化器而不是lambda属性。

    六、索引初始化器

    可以说这个语法糖是集合初始化器的升级版,让基于索引的集合初始化更加合理了。

    现在初始化一个Dictionary可以这么写:

    new Dictionary<int, string>
    {
        [1] = "a",
        [5] = "e"
    };

    键值关系一目了然,而原来要初始化一个Dictionary得这么写:

    new Dictionary<int, string>
    {
        {1, "a"},
        {5, "b"}
    };

    光是一堆大括号就实在惹人吐槽。。。需要注意,集合初始化器与索引初始化器不能混合使用,当然我相信也没人会这么去做。。。
    另外,下面这段代码也能够通过编译,不过运行时会出错:

    new List<string>
    {
        [0] = "a"
    };

    因为对于Dictionary,编译器知道该调用Add方法,而对于List,编译器只知道蠢蠢地对索引器进行赋值。。。
    当然,不支持List的索引初始化一方面是因为集合初始化器的语法可以应付这种情况,另一方面也是因为可能出现这样的情况:

    new List<string>
    {
        [0] = "a",
        [2] = "c"
    };

    很显然List的Add方法没办法完成这项工作。。。

    七、异常过滤器

    这个算是新特性中较为重要也是改动很大的一个部分,先来看看怎么用的:

    try
    {
        throw new IOException("Not Throw");
    }
    catch (IOException ex) when (ex.Message != "Need Throw")
    {
        Console.WriteLine(ex.Message);
    }
    catch (NullReferenceException ex)
    {
        Console.WriteLine(ex.Message);
        throw;
    }

    运行结果:

    Not Throw

    这种过滤如果放在以前就得写得非常难看了:

    try
    {
        throw new IOException("Not Throw");
    }
    catch (IOException ex)
    {
        if (ex.Message != "Need Throw")
        {
            Console.WriteLine(ex.Message);
        }
        else if (ex is NullReferenceException)
        {
            Console.WriteLine(ex2.Message);
            throw;
        }
    else
    {
    throw
    }
    }

    关键在于以前在catch块中捕获的异常没法传给下一个catch块了。
    看一下新版代码的IL吧:

    .try
    {
        IL_0000: ldstr "Not Throw"
        IL_0005: newobj instance void [mscorlib]System.IO.IOException::.ctor(string)
        IL_000a: throw
    } // end .try
    filter
    {
        IL_000b: isinst [mscorlib]System.IO.IOException
        IL_0010: dup
        IL_0011: brtrue.s IL_0017
    
        IL_0013: pop
        IL_0014: ldc.i4.0
        IL_0015: br.s IL_002b
    
        IL_0017: stloc.0
        IL_0018: ldloc.0
        IL_0019: callvirt instance string [mscorlib]System.Exception::get_Message()
        IL_001e: ldstr "Need Throw"
        IL_0023: call bool [mscorlib]System.String::op_Inequality(string, string)
        IL_0028: ldc.i4.0
        IL_0029: cgt.un
    
        IL_002b: endfilter
    } // end filter
    catch
    {
        IL_002d: pop
        IL_002e: ldloc.0
        IL_002f: callvirt instance string [mscorlib]System.Exception::get_Message()
        IL_0034: call void [mscorlib]System.Console::WriteLine(string)
        IL_0039: leave.s IL_0047
    } // end handler
    catch [mscorlib]System.NullReferenceException
    {
        IL_003b: callvirt instance string [mscorlib]System.Exception::get_Message()
        IL_0040: call void [mscorlib]System.Console::WriteLine(string)
        IL_0045: rethrow
    } // end handler

    好像看到了什么不得了的东西,居然出现了一个filter块。看来第一段代码try块构造的异常完全没有进catch块,这一点与以前的处理完全不一样了。
    同时注意到在filter块下面还有一个未标明异常类型的catch块,从内容来看就是对应到C#代码的when后第一个大括号。
    filter块中大概是这么个流程:

    1.检验异常类型,true时走下一步,false时进入空引用异常的catch块
    2.对when中表达式进行计算
    3.endfilter判断上一步的结果,true时进入对应的catch块,false时进入空引用异常的catch块

    可以看到,when的作用就是在catch块前插入一个filter块,而endfilter指令做的事情就是依据堆栈顶的值选择进入这个catch块还是将控制转移到异常处理程序。

    八、静态成员引用

    这个特性很久以前就在Java中出现了,而C#6.0也终于将其引入。
    其实早在引入扩展方法的时候就已经破坏了定义类型可知性,然而扩展方法带来的好处实在太大了。
    使用方法如下:

    using static System.String;
    ...
    Console.WriteLine(Concat("a", "b"));

    注意到Concat方法是来自于String类型,也就是说静态引用针对的是成员而不是类型,using static后面不一定是静态类型。
    这个特效带来的好处当然就是方便省事咯,坏处也很明显,就是比扩展方法有过之而无不及的对定义类型可知性的破坏,所以在使用这个特性的时候还是需要非常谨慎。
    适用的成员必须是所有人都很清楚来由的,比如WriteLine、Format,一看就能知道方法是在Console和String类型中定义,而不是当前类型。

    九、catch、finally中的await

    终于可以在异常处理中愉快地使用异步编程语法糖了:

    private async void Test()
    {
        try
        {
            await new Task<int>(() =>
            {
                return 1;
            });
        }
        catch
        {
            await new Task<int>(() =>
            {
                return 1;
            });
        }
        finally
        {
            await new Task<int>(() =>
            {
                return 1;
            });
        }
    }


    最后祝愿Win10能赶紧普及起来,这样广大的.Net程序员才能真正用上这些神兵利器。

  • 相关阅读:
    [ Algorithm ] N次方算法 N Square 动态规划解决
    [ Algorithm ] LCS 算法 动态规划解决
    sql server全文索引使用中的小坑
    关于join时显示no join predicate的那点事
    使用scvmm 2012的动态优化管理群集资源
    附加数据库后无法创建发布,error 2812 解决
    浅谈Virtual Machine Manager(SCVMM 2012) cluster 过载状态检测算法
    windows 2012 r2下安装sharepoint 2013错误解决
    sql server 2012 数据引擎任务调度算法解析(下)
    sql server 2012 数据引擎任务调度算法解析(上)
  • 原文地址:https://www.cnblogs.com/vd630/p/4731956.html
Copyright © 2011-2022 走看看