本文共 7828 字,大约阅读时间需要 26 分钟。
目录
近年来,C#已经从一种具有一个功能的语言发展成为一种语言,其中包含针对单个问题的许多潜在(语言)解决方案。这既好又坏。很好是因为它给了我们成为开发人员的自由和权力(不会影响向后兼容性),并且由于与决策相关的认知负荷而导致不好。
在本系列中,我们希望了解存在哪些选项以及这些选项的不同之处。当然,在某些条件下,某些人可能有优点和缺点。我们将探索这些场景,并提出一个指南,以便在翻新现有项目时让我们的生活更轻松。
我觉得重要的是要讨论新语言功能的发展方向以及旧语言的位置——让我们称之为已建立 ——这些仍然是首选。我可能并不总是对的(特别是,因为我的一些观点肯定会更主观/是品味问题)。像往常一样,留下评论讨论将不胜感激!
让我们从一些历史背景开始。
属性的概念并非出生于C#。实际上,字段的mutator方法(getter / setter)的概念与软件一样古老,并且在面向对象的编程语言中非常流行。
在Java-ish C#中,不会包含任何用于此类mutator方法的特殊语法。相反,人们会选择加入以下代码:
class Sample{ private string _name; public string GetName() { return _name; } public void SetName(string value) { _name = value; }}
按照惯例,我们总是将Get(getter方法的前缀)或Set(setter方法的前缀)放在“通用”标识符的前面。我们还可以在此处识别关于所使用的签名的共同模式。
一般来说,我们可以说下面的接口可以描述这样一个由getter和setter组成的属性:
interface Property{ T Get(); void Set(T value);}
当然,这样的接口不存在,即使它存在,它也只是一个由两个独立接口组成的复合接口——一个用于getter,一个用于setter。
实际上,例如,只有getter才有意义。这是我们经常寻找的封装。在以下示例中,只有类本身可以确定该_name字段的值。不允许“局外人”执行任何突变,这使得使用的突变器已经有用。
class Sample{ private string _name; public string GetName() { return _name; }}
尽管如此,由于我们已经可以看到很多事情都是按惯例进行的并且非常重复,因此C#语言团队认为我们需要一些基于“经典”mutator方法的语法糖:属性!
非常有用 | 应避免的 |
|
|
从第一版C#语言开始,我们就有了(显式的,即经典的)编写属性的方法。他们修复了之前介绍的惯例,但是,不给我们任何其他好处。我们仍然需要显式地编写方法体(getter和setter方法)。更糟糕的是,我们有相当多的大括号来处理,并且不能,例如,重命名setter值的名称。
class Sample{ private string _name; public string Name { get { return _name; } set { _name = value; } }}
C#中的属性看起来像一个方法,但省略了括号(即方法参数)。它还迫使我们编写一个包含get方法,set方法或两者都要。
虽然这似乎更方便编写(至少更一致),但它仍然计算相同。
这是由Java-ish程序生成的MSIL(编译的中间语言)。
Sample.GetName:IL_0000: nop IL_0001: ldarg.0 IL_0002: ldfld Sample._nameIL_0007: stloc.0 IL_0008: br.s IL_000AIL_000A: ldloc.0 IL_000B: ret Sample.SetName:IL_0000: nop IL_0001: ldarg.0 IL_0002: ldarg.1 IL_0003: stfld Sample._nameIL_0008: ret
使用我们的新C#属性时会产生相同的结果。
Sample.get_Name:IL_0000: nop IL_0001: ldarg.0 IL_0002: ldfld Sample._nameIL_0007: stloc.0 IL_0008: br.s IL_000AIL_000A: ldloc.0 IL_000B: ret Sample.set_Name:IL_0000: nop IL_0001: ldarg.0 IL_0002: ldarg.1 IL_0003: stfld Sample._nameIL_0008: ret
请注意区别?除名称外,它们是相同的。这实际上至关重要。当我们拥有这样的属性时,不再可能尝试使用此名称:
因此,对于C#属性中的每个mutator,我们也会删除一些不直接看到的名称。这一开始似乎很简单,但它带来了一些复杂性(设计时名称与编译时名称),可能并不是真正想要或理解的。
非常有用 | 应避免的 |
|
|
正如我们所见,经典属性只提供一些语法糖来修复创建mutator方法时常用的约定。最后,我们得到的是100%与我们原本写的相同。是的,在元数据属性中也标记为这样,使得可以区分来自属性的方法和明确写入的方法,但是,执行的代码看不到任何差异。
随着更新版本的C#(从第三次迭代开始),添加了一些新概念以提供更多的开发便利性。在下文中,我们将在C#中添加的所有这些post-v1称为“现代方式”。
自动属性提供了一种消除大多数带有“标准”属性的样板的方法。标准属性是由具有getter和setter的字段组成的属性。这里重要的是,需要两种方法,虽然通过使用修饰符(即public,protected,internal和private)可能会有所不同。
考虑以下直接示例替换我们之前的实现:
class Sample{ public string Name { get; set; }}
是的,出于性能原因,我们可能不喜欢这个。原因是该字段实际上是“隐藏的”(即,从编译器插入而没有从我们这边访问)。因此,唯一进入该字段的途径是通过该属性。
这也可以在生成的MSIL中看到:
Sample.get_Name:IL_0000: ldarg.0 IL_0001: ldfld Sample.k__BackingFieldIL_0006: ret Sample.set_Name:IL_0000: ldarg.0 IL_0001: ldarg.1 IL_0002: stfld Sample. k__BackingFieldIL_0007: ret
尽管如此,OOP纯粹主义者会告诉我们,无论如何都不应该直接进行字段访问,并且总是通过mutator方法。因此,从这个观点来看,这实际上是一种很好的做法。.NET性能专家还会告诉我们,这样的自动属性不会受到惩罚,因为JIT会内联该方法,从而导致直接修改。
这种方法都很好吗?并不是的。无法将此方法与设置器中的自定义逻辑混合使用(例如,仅在设置“有效”时才设置该值)。要么我们在标准实现中都有(getter和setter)方法,要么我们需要明确两者。
关于这个的修饰符:
class Sample{ protected string Name { get; private set; }}
外部修饰符(在这种情况下,它是protected)将应用于两个mutator方法。我们不能在这里做过少的限制,例如,不可能给getter一个public修饰符,因为它比已经指定的protected修饰符限制性更小。尽管如此,两者都可以随意调整以限制更多,但是,只有一个mutator方法可以相对于外部(属性)修饰符重新调整。
备注: public,protected和private修饰符情况比较明显,而internal修饰符比较特殊。然而,它比public限制性更强,比private限制性更小,比protected限制性更强,也更少。原因很简单:虽然protected可以在当前程序集之外访问(即限制较少),但它也阻止了对当前程序集中非继承类的访问(即限制性更强)。
虽然理论上我们可以对两种mutator方法应用修饰符,但C#语言有充分的理由禁止它。我们应该指定一个清晰可信的可访问性模式,不包括混合访问器。
非常有用 | 应避免的 |
|
|
通常,我们唯一的愿望是反映某个字段的属性。我们不希望任何setter mutator。不幸的是,使用前面的方法,我们没有得到该字段,也无法删除或省略setter。
幸运的是,第一个提案已经解决了这个问题。我们可以自由地省略两种mutator方法中的一种。
让我们看看这个动作:
class Sample{ private string _name = "Foo"; public string Name { get { return _name; } }}
嗯,这有什么用?除了现在仅限于通过字段设置值(显然在设置值时没有隐藏的魔法),我们现在看不到任何明显的优势。
为了完整起见,构造的MSIL看起来像:
Sample.get_Name:IL_0000: nop IL_0001: ldarg.0 IL_0002: ldfld Sample._nameIL_0007: stloc.0 IL_0008: br.s IL_000AIL_000A: ldloc.0 IL_000B: ret Sample..ctor:IL_0000: ldarg.0 IL_0001: ldstr "Foo"IL_0006: stfld Sample._nameIL_000B: ldarg.0 IL_000C: call System.Object..ctorIL_0011: nop IL_0012: ret
在这种情况下的主要优点是能够只读取字段(或给它任何其他任意属性,修饰符或初始化逻辑)。
class Sample{ // will always have the value "Foo" private readonly string _name = "Foo"; public string Name { get { return _name; } }}
与public string Name { get; private set; }方法相比,我们可以肯定地排除在初始化之后设置值的可能性(此保证不仅在未来传达给一个自己,而且还传递给可能跨越该字段的任何其他开发者)。
非常有用 | 应避免的 |
|
|
现在我们所需要的是一种结合我们对readonly字段/属性和自动属性进行编写的愿望的方法。
在C#6中,语言设计团队接受了这个想法并提供了解决问题的方法。C#现在只允许使用getter属性。
实际上,这些属性可以像readonly字段一样分配,例如,在构造函数中,或者在声明它们时直接分配。
让我们看看这个动作:
class Sample{ public string Name { get; } = "Foo";}
生成的MSIL类似于自动属性(谁会猜到),但没有任何setter方法。相反,我们看到的是对已生成的基础字段的分配。
Sample.get_Name:IL_0000: ldarg.0 IL_0001: ldfld Sample.k__BackingFieldIL_0006: ret Sample..ctor:IL_0000: ldarg.0 IL_0001: ldstr "Foo"IL_0006: stfld Sample. k__BackingFieldIL_000B: ldarg.0 IL_000C: call System.Object..ctorIL_0011: nop IL_0012: ret
如果我们将此代码与我们的显式(只读)字段的代码进行比较,我们会发现两者在初始化时都是相同的。这里没有功能差异,但是,对于getter来说,代码更小,更直接。原因是,由于C#编译器负责生成代码(即带访问的返回字段),因此它将跳过一些验证/安全调用。出于性能原因,我们可能会说这对当前版本来说是一个优势,但请记住,我们在这里只能看到未经优化的non-jitted代码。实际上,JIT可以删除所有以前的样板文件并内联剩余的字段加载。
非常有用 | 应避免的 |
|
|
考虑到这一点,我们可以在提高灵活性的同时变得更加简单吗?
C#3的一个重要特性是引入了LINQ。有了它,就引入了一整套新的语言功能。其中一个很棒的特性是用于编写匿名函数的lambda语法(在C#中,我们也可以将这些函数引用称为委托,而在例如C ++中它们被称为仿函数)。这种lambda语法一直是C#7中的一个核心元素,使C#对函数式编程(FP)中的模式更具功能性/友好性。
其中一个增强功能是C#属性,现在可以使用“胖箭头”(即lambda语法)将其解析为表达式。
这可以看起来像下面的代码一样简单:
class Sample{ private readonly string _name = "Foo"; public string Name => _name;}
我们也可以(ab)使用这种语法来提出更微不足道的东西,例如,public string Name => "Foo"在这种特殊情况下效果更好,但通常不一样或建议。
然而,在某些情况下,属性只是围绕某些其他功能(例如,延迟加载)的浅包装,这种语法可能是理想的。
Sample.get_Name:IL_0000: ldarg.0 IL_0001: ldfld Sample._nameIL_0006: ret Sample..ctor:IL_0000: ldarg.0 IL_0001: ldstr "Foo"IL_0006: stfld Sample._nameIL_000B: ldarg.0 IL_000C: call System.Object..ctorIL_0011: nop IL_0012: ret
请注意,MSIL看起来像在readonly属性情况下一样直接?我们谈到的额外验证是使用块语句给出的安全保证。现在我们只使用表达式并省略该块。这是事先不可能的,因此MSIL必须反映块,现在它可以更简单。
非常有用 | 应避免的 |
|
|
如果我们想使用上面显示的表达式语法,但该属性还需要一个setter方法呢?
幸运的是,C#语言设计团队也考虑过这个案例。我们实际上可以使用标准属性(在C#1.0中找到)和表达式语法的组合。
在实践中,这看起来如下:
class Sample{ private string _name = "Foo"; public string Name { get => _name; set => _name = value; }}
修饰符也不是问题,并且在原始C#规范中自然添加。MSIL没有给我们任何惊喜。
Sample.get_Name:IL_0000: ldarg.0 IL_0001: ldfld Sample._nameIL_0006: ret Sample.set_Name:IL_0000: ldarg.0 IL_0001: ldarg.1 IL_0002: stfld Sample._nameIL_0007: ret Sample..ctor:IL_0000: ldarg.0 IL_0001: ldstr "Foo"IL_0006: stfld Sample._nameIL_000B: ldarg.0 IL_000C: call System.Object..ctorIL_0011: nop IL_0012: ret
实际上,getter比原始版本更简单,甚至setter也从不在块语句中获益(4而不是5条指令)。
非常有用 | 应避免的 |
|
|
C#的演变并没有停止在属性上。一旦将其作为一个更安全的约定添加到语言中,它们就演变为功能性的结构,公开字段,并使延迟加载这样的事情变得令人愉快。
下一篇:
原文地址:
转载地址:http://rczhj.baihongyu.com/