目录
介绍
背景
什么是属性?
从Java到C#
经典方式
现代方式
自动属性
分配的属性
只读属性
属性表达式
Get和Set表达式
结论
近年来,C#已经从一种具有一个功能的语言发展成为一种语言,其中包含针对单个问题的许多潜在(语言)解决方案。这既好又坏。很好是因为它给了我们成为开发人员的自由和权力(不会影响向后兼容性),并且由于与决策相关的认知负荷而导致不好。
在本系列中,我们希望了解存在哪些选项以及这些选项的不同之处。当然,在某些条件下,某些人可能有优点和缺点。我们将探索这些场景,并提出一个指南,以便在翻新现有项目时让我们的生活更轻松。
背景我觉得重要的是要讨论新语言功能的发展方向以及旧语言的位置——让我们称之为已建立 ——这些仍然是首选。我可能并不总是对的(特别是,因为我的一些观点肯定会更主观/是品味问题)。像往常一样,留下评论讨论将不胜感激!
让我们从一些历史背景开始。
什么是属性?属性的概念并非出生于C#。实际上,字段的mutator方法(getter / setter)的概念与软件一样古老,并且在面向对象的编程语言中非常流行。
从Java到C#在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#属性)
从第一版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._name
IL_0007: stloc.0
IL_0008: br.s IL_000A
IL_000A: ldloc.0
IL_000B: ret
Sample.SetName:
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldarg.1
IL_0003: stfld Sample._name
IL_0008: ret
使用我们的新C#属性时会产生相同的结果。
Sample.get_Name:
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldfld Sample._name
IL_0007: stloc.0
IL_0008: br.s IL_000A
IL_000A: ldloc.0
IL_000B: ret
Sample.set_Name:
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldarg.1
IL_0003: stfld Sample._name
IL_0008: ret
请注意区别?除名称外,它们是相同的。这实际上至关重要。当我们拥有这样的属性时,不再可能尝试使用此名称:
因此,对于C#属性中的每个mutator,我们也会删除一些不直接看到的名称。这一开始似乎很简单,但它带来了一些复杂性(设计时名称与编译时名称),可能并不是真正想要或理解的。
非常有用
应避免的
- 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__BackingField
IL_0006: ret
Sample.set_Name:
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: stfld Sample.k__BackingField
IL_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#语言有充分的理由禁止它。我们应该指定一个清晰可信的可访问性模式,不包括混合访问器。
非常有用
应避免的
- 简单字段作为属性
- 简单而简洁的字段+属性创建
- 非简单字段(例如,只读)
- 需要定制逻辑/行为
- 将生成的getter / setter与未生成的getter / setter混合
通常,我们唯一的愿望是反映某个字段的属性。我们不希望任何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._name
IL_0007: stloc.0
IL_0008: br.s IL_000A
IL_000A: ldloc.0
IL_000B: ret
Sample..ctor:
IL_0000: ldarg.0
IL_0001: ldstr "Foo"
IL_0006: stfld Sample._name
IL_000B: ldarg.0
IL_000C: call System.Object..ctor
IL_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__BackingField
IL_0006: ret
Sample..ctor:
IL_0000: ldarg.0
IL_0001: ldstr "Foo"
IL_0006: stfld Sample.k__BackingField
IL_000B: ldarg.0
IL_000C: call System.Object..ctor
IL_0011: nop
IL_0012: ret
如果我们将此代码与我们的显式(只读)字段的代码进行比较,我们会发现两者在初始化时都是相同的。这里没有功能差异,但是,对于getter来说,代码更小,更直接。原因是,由于C#编译器负责生成代码(即带访问的返回字段),因此它将跳过一些验证/安全调用。出于性能原因,我们可能会说这对当前版本来说是一个优势,但请记住,我们在这里只能看到未经优化的non-jitted代码。实际上,JIT可以删除所有以前的样板文件并内联剩余的字段加载。
非常有用
应避免的
- 公开readonly变量作为属性
- 需要直接访问或修改字段
- 需要灵活实施的情况
考虑到这一点,我们可以在提高灵活性的同时变得更加简单吗?
属性表达式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._name
IL_0006: ret
Sample..ctor:
IL_0000: ldarg.0
IL_0001: ldstr "Foo"
IL_0006: stfld Sample._name
IL_000B: ldarg.0
IL_000C: call System.Object..ctor
IL_0011: nop
IL_0012: ret
请注意,MSIL看起来像在readonly属性情况下一样直接?我们谈到的额外验证是使用块语句给出的安全保证。现在我们只使用表达式并省略该块。这是事先不可能的,因此MSIL必须反映块,现在它可以更简单。
非常有用
应避免的
- 计算属性(标准,即仅限getter)
- 详细地公开一个字段
- 包装只读变量
- 公开任意变量
如果我们想使用上面显示的表达式语法,但该属性还需要一个setter方法呢?
Get和Set表达式幸运的是,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._name
IL_0006: ret
Sample.set_Name:
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: stfld Sample._name
IL_0007: ret
Sample..ctor:
IL_0000: ldarg.0
IL_0001: ldstr "Foo"
IL_0006: stfld Sample._name
IL_000B: ldarg.0
IL_000C: call System.Object..ctor
IL_0011: nop
IL_0012: ret
实际上,getter比原始版本更简单,甚至setter也从不在块语句中获益(4而不是5条指令)。
非常有用
应避免的
- 具有setter方法的计算属性
- 超灵活,轻巧简洁
- 没有特殊逻辑的属性(即直接暴露字段)
- 没有getter或setter的属性
- 公开只读字段
C#的演变并没有停止在属性上。一旦将其作为一个更安全的约定添加到语言中,它们就演变为功能性的结构,公开字段,并使延迟加载这样的事情变得令人愉快。
下一篇:使C#代码现代化——第二部分:方法
原文地址:https://www.codeproject.com/Articles/1278754/Modernize-Your-Csharp-Code-Part-I-Properties