目录

介绍

背景

什么是属性?

从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>
{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.<Name>k__BackingField
IL_0006:  ret         Sample.set_Name:
IL_0000:  ldarg.0
IL_0001:  ldarg.1
IL_0002:  stfld       Sample.<Name>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.<Name>k__BackingField
IL_0006:  ret         Sample..ctor:
IL_0000:  ldarg.0
IL_0001:  ldstr       "Foo"
IL_0006:  stfld       Sample.<Name>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

使C#代码现代化——第一部分:属性相关推荐

  1. netbeans代码提示_Java代码现代化的七个NetBeans提示

    netbeans代码提示 在" 七个不可或缺的NetBeans Java提示"一文中 ,我谈到了一般使用NetBeans提示的问题,然后重点介绍了七个提示. 接下来列出了该帖子中强 ...

  2. 使C#代码现代化——第二部分:方法

    目录 介绍 背景 什么是方法? 现代方式 扩展方法 委托 Lambda表达式 LINQ表达式 方法表达式 局部函数 结论 介绍 近年来,C#已经从一种具有一个功能的语言发展成为一种语言,其中包含针对单 ...

  3. 可视化指挥调度系统_如何通过视力调度使预订系统现代化

    可视化指挥调度系统 This article was sponsored by Acuity Scheduling. Thank you for supporting the sponsors who ...

  4. 《驯服烂代码》第一章

    [按]这是本人正在撰写的<驯服烂代码>第一章的草稿,请各位网友审阅,望各位不吝赐教,多谢! 第1章 何谓烂代码 光阴如梭,从1993年大学计算机专业本科毕业,至今一晃就过了近20年.在这近 ...

  5. android java style_Android 在Java代码中设置style属性--使用代码创建ProgressBar对象

    强烈推荐: 在andriod开发中,很大一部分都要与资源打交道,比如说:图片,布局文件,字符串,样式等等.这给我们想要开发一些公共的组件带来很大的困难,因为公共的组件可能更愿意以jar包的形式出现.但 ...

  6. 如何在Javascript中访问对象的第一个属性?

    本文翻译自:How to access the first property of an object in Javascript? Is there an elegant way to access ...

  7. Operating System-Thread(5)弹出式线程使单线程代码多线程化会产生那些有关问题

    Operating System-Thread(5)弹出式线程&&使单线程代码多线程化会产生那些问题 本文主要内容 弹出式线程(Pop-up threads) 使单线程代码多线程化会产 ...

  8. CSS(一):CSS概述、CSS引入方式、CSS优先级、CSS代码格式、CSS属性;CSS选择器;尺寸和颜色单位、背景和文本设置

    目录 一.CSS 1.1 CSS概述 1.1.1 定义 1.1.2 特征 1.1.3 组织网页的两种常用方式 1.2 引入CSS的四种方式 1.2.1 Style属性方式(内联样式) 1.2.2 St ...

  9. security_huks模块下hks_rkc.c代码评注第一部分

    security_huks模块下hks_rkc.c代码评注第一部分 本篇综述 变量定义与初始化 密钥库文件属性初始化 初始化熵 填充密钥库文件缓冲区 填充哈希字段到缓冲区 填充根密钥数据到缓冲区 填充 ...

最新文章

  1. 为什么php动态语言,动态语言静态化
  2. python使用笔记:pyperclip模块安装和使用
  3. 徐教授的对于商业模式创新的讲座!十分有用!
  4. 前端性能优化之防抖-debounce
  5. git checkout 命令详解
  6. python怎么设置颜色深浅变化_Python赋值、深浅copy
  7. 一步一步学Ruby(二十一):文件操作2
  8. 为什么tomcat在eclipse中启动了,访问不了
  9. Paper Pal:一个中英文论文及其代码大数据搜索平台
  10. 面向对象三之对象的使用方法
  11. 身份证过期了银行卡还能用吗?
  12. 微信小程序二维码生成
  13. 小议新版GB9706.1-2020的基本性能
  14. 360面临两线作战----手机安全领域硝烟再起
  15. 指数分布的期望和方差推导
  16. SEM1 PSYCHOLOGY LEC2
  17. 【docker-gpu】报错:W: GPG error:xxx, InRelease: The following signatures couldn‘t be verified because th
  18. v2021年烷基化工艺考试题及烷基化工艺考试试卷
  19. 3D动作绑定_3D动漫制作软件,你知道几个?
  20. 电商物流系统技术架构进化史

热门文章

  1. fpga供电电压偏低会怎样_正点原子【FPGA-开拓者】第三章 硬件资源详解
  2. python中json使用方法总结_python中的json总结
  3. 如何为活动设计海报|优秀案例,分享关键技巧
  4. 「PPT模板」 商务UI风格
  5. edxp显示未安装_智能水表安装使用注意事项
  6. C++四种强制类型转换解析
  7. Linux BPF hello world C语言示例代码
  8. 【linux指令】dialog实现终端下的GUI-2
  9. leetcode题库:3.无重复字符的最长子串
  10. 富文本编辑器quill的集成