第4章 类型设计准则

image

第4章 类型设计准则

  • DO​:类应该由一组定义明确、相互关联的成员组成。

    一个类,如果能用一句话描述清楚它的用途,那么它的设计是优秀的。

1 类型(class、struct)和 namespace

  • DO​:namespace 用于组织类,通过 namespace 将相关功能按层次铺开,但不要有过深的层次、过多的数量。

    要做到让开发人员很容易地浏览框架并找到想要的 API,过深的层次、过多的数量都不便于开发者浏览。

    namespace 的主要目的不是解决同名类型的名字冲突,而是为了把类型组织成一个有条理的、易于浏览、易于理解的层次结构。
    如果单个框架中出现类型名冲突,意味着框架设计得很烂。名字相同的类型要么应该合并起来,要么重命名以改进代码的可读性和可搜索性。

  • AVOID​:高级方案的 API 应和常见方案的 API 分隔开,放在不同的 namespace 中。

    用户能更容易理解框架基本概念,也更容易在常见场景中使用框架。

    高级的类应该放在简单类所在 namespace 的子 namespace 中,如:System.Mail​ 存放着简单类,System.Mail.Advanced​ 存放着高级类。

  • DON'T​:定义类型前要先指定类型的 namespace。

    不要每个类都使用默认的命名空间,这有助于可能存在的类型名冲突。不过,这不意味着可以引入这一的冲突,具体原因第 3 章已经讲过。

标准子 namespace 的命名

很少使用的类型应放在子 namespace 中,以下是我们过去的命名经验,可以参考

.Design 子命名空间

  • DO​:为基本类型提供设计功能的类型应放在. Design 后缀的 namespace 中。

    System.Windows.Forms.Design
    System.Messageing.Design
    System.Diagnostics.Design
    

.Permissions 子名字空间

  • DO​:权限类型应放在. Permissions 后缀的 namespace 中。

.Interop 子名字空间

  • DO​:为基本 namespace 提供互操作功能的类型应放在 . Interop 后缀的 namespace 中。

  • DO​:所有位于主互操作程序集中的代码应放在 . Interop 后缀的 namespace 中。

2 class 和 struct 之间的选择

  • CONSIDER​:一个类型,如果它实例较 且生命周期较 ,或经常被 内嵌 在其他对象中(尤其是 数组 ),可以使用结构体。

  • AVOID​:尽可能少使用结构体,使用结构体应遵循如下原则:

    • 它在逻辑上表示单个值,类似于基础类型(int、double 等)。
    • 它的实例大小小于 24 byte。
    • 它是不可变的。
    • 它不会被频繁装箱。

    除此以外,所有类型应该被定义为类。

3 class 和 interface 之间的选择

  • DO​:优先使用 ,而非 接口

    基于类的 API 更容易被改进,它可以在不破坏已有代码的情况下向类中添加成员。

  • DO​:要使用 抽象类 来分离协议与实现,而非 接口

    抽象类经过正确的设计,同样能够解除契约与实现之间的耦合,与接口达到的效果不相上下。

    当然,这不是说接口一无是处。如果契约不会随着时间改变,接口就非常合适;如果为一个族类型定义公共基类,抽象类更好。

  • DO​:如果结构体(struct)需要实现多态,应使用 接口

    值类型不能继承,但可以实现接口。如 IComparable​​、IFormattable​​、IConvertible​​:

    public struct Int32 : IComparable, IFormattable, IConvertible { ... }
    public struct Int64 : IComparable, IFormattable, IConvertible { ... }
    

定义良好的接口的标志:

  1. 它不会限定于类型,更像一个类型的“ 属性 ”;
  2. 每个接口只做一件事。

  • CONSIDER​:可以通过接口实现多重继承的效果。

    System.Drawing.Image ​实现了 System.IDisposable ​和 System.ICloneable ​接口,因此它是可以处置的(disposable)、可复制的(clonable),还继承了 MarshalByRefObject​:

    public class Image : MarshalByRefObject, IDisposable, ICloneable { ... }
    

4 abstract class 的设计

  • DON'T​​:不要在抽象类中定义访问类型为 public ​、 protected-internal ​的构造函数。

    抽象类禁止进行实例化,因此不应该为抽象类定义访问类型为 public、protected-internal 的构造函数。这么做不光是错误的,还会误导用户。

    // 坏设计
    public abstract class Claim {public Claim() { ... }
    }// 好设计
    public abstract class Claim {protected Claim() { ... }
    }
    

  • DO​​:抽象类应定义访问类型为 protectedinternal 的构造函数。

    protected 构造函数更常见,可以做到仅允许子类调用它。

    public abstract class Claim {protected Claim() { ... }
    }
    

    internal 构造函数,可以做到仅程序集内的子类调用它,我们可以在该构造函数中反向操作子类,以实现一些特殊的设计模式,构造函数又不会因其他程序集实现该抽象类而遭到滥用。

    public abstract class Claim {internal Claim() { ... }
    }
    

    Tips

    即使我们未定义构造函数,CLR 也会为我们隐式创建一个 protected 无参构造函数。

  • DO​:public​/protected ​抽象类应至少有一个实现。

    这有助于验证抽象类的设计是否正确。如:System.IO.FileStream ​是 System.IO.Stream ​抽象类的一个实现。

5 static class 的设计

  • DO​:少用静态类。

    静态类应作为辅助类,用于辅助框架的面向对象的核心。

  • DON'T​:不要把静态类当作杂物桶。

    每个静态类都应该有其明确的角色划分。如果你对类的描述包含了“和”这样的连词,或包含一个全新的句子,说明你需要另外一个类。

6 interface 的设计

  • DO​​:如果值类型需要支持一些公共 API,要通过 接口 实现。

    int ​实现了 IComparable ​接口。

  • CONSIDER​​:需要多重继承时,可以通过 接口 实现。

  • AVOID==: 避免==使用记号接口(没有成员的接口)。

    最好使用自定义 Attribute​​而非记号接口:

    // 避免
    public interface IImmutable {} // 记号接口public class Key : IImmutable { ... }// 考虑
    [IMutable]
    public class Key { ... }
    

    使用 Attribute 也有缺点:

    1. 开销更大;
    2. 编译时无法发现 class 是否标注了 Attribute。

    如果我们需要编译时检查,则可以使用记号接口:

    public interface ITestSerializable {} // 记号接口
    public void Serialize (ITextSerializable item){// 通过反射序列号公有属性
    }
    

  • DO​:接口应至少有一个实现,有一个调用(一个以该接口为参数的方法,或一个该类型的属性)

    这有助于验证接口的设计和实现。

  • DON'T ​​: 禁止 给已发行的接口再添加成员。

    这样会破坏接口的实现。为避免版本问题,应该创建一个新的接口。

7 struct 的设计

  • DON'T ​​: 不要 为结构体提供默认构造函数。

    否则会造成 default(SomeStruct)​ 和 new SomeStruct()​ 行为不同。

    附注:.NET Framework 中 C# 禁止自定义默认构造函数

  • DO​​:要定义不可变的值类型,并使用 readonly 修饰符声明。

    可变值类型在传值时会 传递副本(副本隐式创建) ,使用者可能意识不到他们修改的是副本,而非源值。

    readonly 修饰符在某些操作上能够避免 防御性拷贝

    // 坏设计
    public struct ZipCode {public int FiveDigitCode { get; set; }    // get、set都有public int PludsFourExtension { get; set; }
    }// 好设计
    public readonly struct ZipCode {public ZipCode(int fiveDigitCode, int plusFourExtension) { ... }public ZipCode(int fiveDigitCode) : this(fiveDigitCode, 0) { }public int FiveDigitCode { get; }    // 只有getpublic int PludsFourExtension { get; }public override string ToString() {...}
    }public partial class Other{private readonly ZipCode _zipCode;...private void Work(){// 因ZipCode是readonly struct,调用ToString()不会带来防御性拷贝string zip = _zipCode.ToString();}
    }
    

  • DO​​:对于可变值类型,声明不可变方法。

    调用值类型的方法时,可以避免 防御性拷贝

    // 可变值类型
    public struct ZipCode {private int _plusFour;public int FiveDigitCode { get; set; }// 显式只读,调用get时得到的不再是拷贝public int PludsFourExtension {readonly get => _plusFour;set { ... }}// 不可变方法,调用它时不再进行拷贝public override readonly string ToString() { ... }
    }
    

    使用 readonly​ 修饰 struct​ 时,C# 编译器会自动将 readonly​ 应用到每个方法和属性的 get​ 方法。

  • DO​​​:当结构实例为 默认 值(如 0、false、null)时,结构仍处于有效状态

    // 坏设计
    public struct PositiveInteger {int value;public PositiveInteger(int value) {if (value <= 0) throw new ArgumentException(...);this.value = value;}public override string ToString() {return value.ToString();}
    }// 好设计
    public struct PositiveInteger {int value;    // 逻辑值是 value+1public PositiveInteger(int value) {if (value <= 0) throw new ArgumentException(...);this.value = value - 1;}public override string ToString() {return (value+1).ToString();}
    }
    

  • DON'T​​​:不要定义类似于 ref struct​ 类型的值类型,除非是在性能至关重要的特定底层使用场景中。

    ref struct​ 只允许存在于栈中,不能被装箱至堆中。因此,它不能被作为其他类型中字段的类型使用(除非该类型也是 ref struct​),也不能用于 async 声明的异步方法中

  • DO​​:值类型需要实现 IEquatable<T> ​​。

    IEquatable<T>​ 可以避免以下问题:

    1. 值类型的 Object.Equals ​方法会导致装箱,
    2. Object.Equals ​使用了反射,它的默认实现效率不高。

8 枚举的设计

  • DO​​​:要使用 枚举 来加强那些表示值的集合的参数、属性以及返回值的类型。

  • DO​​:要使用枚举代替静态常量。

    // 避免
    public static class Color {public static const int Red      = 0;public static const int Green    = 1;public static const int Blue     = 2;...
    }// 推荐
    public enum Color {Red,Green,Blue,...
    }
    

  • DON'T​​:不要使用枚举来定义开放集合。

    比如操作系统的版本、朋友的名字等。

  • DON'T​​:不要设保留值。

    保留值只会污染实际值的集合,还会误导用户。

    public enum DeskType{Circular,Oblong,Rectangular,// 以下保留值不应该存在ReservedForFutureUse1,ReservedForFutureUse2,
    }
    

  • AVOID​​:避免创建只有一个值的枚举。

    我们可以用方法重载在日后添加参数,而非用单值枚举做占位.

    // 坏设计
    public enum SomeOption {DefaultOption// 我们日后将添加其他值
    }
    ...// option不是必须的。未来任何时候都可以通过重载实现该方法
    public void SomeMethod(SomeOption option) {...
    }
    

  • DON'T​:禁止在枚举中包含 sentinel(哨兵)值

    sentinel 值用来跟踪枚举的状态,却不属于枚举所表示值的集合。

    // 坏做法
    public enum DeskType{Circular     = 1,Oblong       = 2,Rectangular  = 3,LastValue    = 3,  // 不应包含sentinel值
    }public void OrderDesk(DeskType desk){if (desk > DeskType.LastValue) throw new ArgumentOutOfRangeException(...);...
    }// 好做法
    public void OrderDesk(DeskType desk){if (desk > DeskType.Rectangular || desk < DeskType.Circular) throw new ArgumentOutOfRangeException(...);...
    }
    

  • DO​:要为简单枚举提供一个零值。

    应该把枚举中 最常用的默认值 赋值为零。

    public enum Compression {None = 0,GZip,Deflate,
    }
    public enum EventType {Error = 0,Warning,Information,...
    }
    

  • CONSIDER​​:以 Int32​ 为载体实现枚举。

    例外:

    • 该枚举是标记枚举,且超过 32 个标记,或预计今后会有更多的标记。

    • 需要与非托管代码进行交互,且非托管代码使用非 Int32 的枚举(即非 4 字节)。

    • 为了节省内存:

      • 枚举用作 struct 或 class 的字段,且会被频繁实例化;
      • 枚举用于创建大型数组或集合;
      • 枚举的大量实例用于序列化。

  • DO​:标记枚举用复数名词命名;简单枚举用单数名词命名。

1 标记枚举的设计

  • DO​​:标记枚举应使用 System.FlagsAttribute ​​标记。

    [Flags]
    public enum AttributeTargets {...
    }
    

  • DO​​:用 2 的幂次方 作为标记枚举的值。

    这样可以使用位操作符自由的组合。

    [Flags]
    public enum WatcherChangeTypes {None = 0,Created = 0x0002,Deleted = 0x0004,Changed = 0x0008,Renamed = 0x0010,
    }
    

    我们还可以这样做:

    [Flags]
    public enum WatcherChangeTypes {None = 0,Created = 1 << 1,Deleted = 1 << 2,Changed = 1 << 3,Renamed = 1 << 4,
    }
    

  • CONSIDER​​:为常用的组合标记提供特殊的枚举值。

    [Flags]
    public enum FileAccess {Read = 1,Write = 2,ReadWrite = Read | Write,
    }
    

  • AVOID​​:标记枚举不应包含某些无效组合。

    System.Reflection.BindingFlags​ 枚举就是这种错误设计的例子。该枚举试图表示许多不同的概念,如可见性、静态性、成员类型等:

    // 原设计
    [Flags]
    public enum BindingFlags {Default = 0,Instance = 0x4,Static = 0x8,Public = 0x10,NonPublic = 0x20,CreateInstance = 0x200,GetField = 0x400,SetField = 0x800,GetProperty = 0x1000,SetProperty = 0x2000,InvokeMethod = 0x100,...
    }
    

    其中一些枚举值的组合是无效的,如 Type.GetMembers ​方法以该枚举为参数,但必须指定 BindingFlags.Instance ​或 BindingFlags.Static​。

    好的做法是将该枚举值分成两个或更多个枚举或其他类型:

    // 好设计
    [Flags]
    public enum Visibilities {None = 0,Public = 0x10,NonPublic = 0x20,
    }[Flags]
    public enum MemberScopes {None = 0,Instance = 0x4,Static = 0x8,
    }[Flags]
    public enum MemberKinds{None = 0,Constructor = 1 << 0,Field = 1 << 1,PropertyGetter = 1 << 2,PropertySetter = 1 << 3,Method = 1 << 4,
    }public class Type {public MemberInfo[] GetMembers(MemberKinds members, Visibilities visibility, MemberScopes scope);)
    }
    

  • AVOID​​: 0 不能作为标记枚举的值。(表示“清除标记”除外)

  • DO​​:标记枚举的 0 值应命名为 None ,该值必须始终表示“ 所有标记均被清除 ”.

    [Flags]
    public enum BorderStyle {Fixed3D        = 0x1,FixedSingle    = 0x2,None           = 0x0
    }
    

    0 值之所以特殊,是因为不进行赋值操作时,枚举成员默认为 0。因此在设计时要考虑到这一点。特别是,0 值应该是以下两者之一:

    • 常用的默认值
    • 表示错误的值,且 API 会检查这个错误值

2 给枚举添加值

  • CONSIDER​:建议为枚举添加新值,尽管要冒一点兼容性风险。

    如果添加新值会导致应用程序不兼容,则可以考虑添加一个新的 API 来返回新老枚举值,同时要求用户停止使用老 API(仍返回老枚举值)。

9 嵌套类型

  • DO​:当嵌套类型与其外部类型之间的关系需要 成员可访问性语义 时,才使用嵌套类型。

  • DON'T​​:不要使用嵌套类型进行逻辑分组,应使用 namespace 分组。

  • AVOID​​:避免公开暴漏嵌套类型。

    唯一的例外是:在极少数情况下,比如在子类化或其他高级的自定义场景下,需要声明嵌套类型的变量。

  • DON'T​:如果一个类不仅外层类会用,其他类也会使用,则不应该定义为嵌套类型。

  • DON'T​​:不要将嵌套类型定义为 接口 的成员。

    许多语言不支持这种构造模式。

10 类型和程序集元数据

  • DO​:在包含公共类型的程序集中使用 CLSCompliant(true) ​ 特性。

    该特性用于声明该程序集中的类型是符合 CLS 规范的,可以为所有.NET 编程语言所使用。

    [assembly:ClSCompliant(true)]
    

  • DO​:在包含 公共 类型的程序集中使用 AssemblyVersionAttribute​ 特性。

    [assembly:AssemblyVersion(...)]
    

  • DO​​:要将下列信息特性应用到程序集中。

    Visual Studio 会识别这些信息,告知用户程序集内容:

    [assembly:AssemblyTitle("System.Core.dll")]
    [assembly:AssemblyCompany("Microsoft Corporation")]
    [assembly:AssemblyProduct("Microsoft .NET Framework")]
    [assembly:AssemblyDescription(...)]
    

  • CONSIDER​​:在程序集版号中使用 的格式。

    V:主版本号

    S:服务版本号

    B:构建号

    R:构建修订号

    [assembly:AssmeblyVersion("3.5.21022.8")]
    

    附注:现如今更常用的版本控制是语义化版本 2.0.0 | Semantic Versioning (semver.org)

  • CONSIDER​:使用 ComVisible(false) ​ 标注不允许 COM 调用。

    可供 COM 调用的 API 需要特别设计,不应该将.NET 程序集暴漏给 COM。如果 COM 确实需要调用该 API,可以在该 API 中或整个程序集中使用 ComVisible(true)​​。

  • CONSIDER​:在程序集中使用 AssemblyFileVersionAttribute​ 和 AssemblyCopyrightAttribute​。

11 强类型字符串

  • CONSIDER​​​:当基类所支持的一组固定输入参数(枚举),不能满足 派生 类需要的参数时,建议使用强类型字符串。

  • DO​​​:要将强类型字符串声明成带有 字符串 构造函数的不可变 类型( readonly struct )。

    强类型字符串要遵循 不可变值 类型的准则

  • DO:要允许 构造函数接收空白输入。

强类型字符串是 struct,而 struct 可以零初始化(即使用默认构造函数初始化)。构造函数接收空白输入应等效于零初始化。

  • DO​​​:对于已知选项,应通过 static readonly 属性 声明至该类型中。

    这样可以提供类似于枚举的 InteliSense 体验。

  • DO​​​:要覆写 ToString() 方法返回隐含的字符串值。

    便于调试。

  • CONSIDER​​​:建议通过一个 只读 属性来暴露强类型字符串所隐含的字符串值。

    因为 ToString()​ 是为调试服务的,如果开发者确实需要知道该值,使用属性获取更好。属性名没有惯例/准则,可以使用“ Value ”作为属性名。

    如果开发者很少或从来不用该属性,不定义属性会让这个类看起来 更像枚举

  • DO​​​:要覆写 相等 运算符。

    通过覆写 operator == ​​​,可以让强类型字符串看起来像 字符串枚举 。通常,强类型字符串应利用字符串相等性来比较相等,如果强调不区分大小写,则可以通过覆写 相等 运算实现。

  • AVOID​​​​:避免提供强类型字符串和 System.String ​的重载。

    这种重载虽然便于调用(省去 new 一个对象的功夫),但会让新手开发者困惑。

建议:如果在原有 API 上新定义了强类型字符串,用于辅助有效输入,可以考虑将原先基于 System.String​ 的重载方法标记为 [EditorBrowsable(EditorBrowsableState.Advanced)] ​、 [EditorBrowsable(EditorBrowsableState.Never)] ​或 [Obsolete]

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/866790.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

BurpSuite实操之代理功能

一、代理原理Burp Suite代理工具是以拦截代理的方式,拦截所有通过代理的网络流量,如客户端的请求数据、服务器端的返回信息等。通过拦截,Burp Suite以中间人的方式,可以对客户端请求数据、服务端返回做各种处理,以达到安全评估测试的目的。在日常工作中,我们最常用的web客…

window11安装安卓子系统,畅玩安卓软件

在Windows 11刚推出时,微软便宣称该操作系统中可以直接安装运行安卓APK应用程序,如同Android虚拟机一样,不过是要实现这一功能,我们必须在Windows 11中单独安装Windows 11安卓子系统,这里说明一下其标准的名称为:适用于 Android™️ 的 Windows 子系统(Windows Subsyste…

Python + Appium 自动化操作微信入门(超详细)

Appium是一个开源的自动化测试工具,支持 Android、iOS 平台上的原生应用,支持 Java、Python、PHP 等多种语言。Appium是一个开源的自动化测试工具,支持 Android、iOS 平台上的原生应用,支持 Java、Python、PHP 等多种语言。 Appium 封装了 Selenium,能够为用户提供所有常见…

浅谈STC单片机看门狗的作用和使用成都控制器开发

有的控制器的运行环境是很复杂的,比如可能突然来个电压,突然来个浪涌,周围存在的强电磁场也可能交替对控制器造成冲击,也有可能外部器件出现错误很久不响应,这样就有可能让单片机的程序跑飞,或者进入死循环出不来,那这就完蛋了。怎么办呢? 一个简单的办法就是使用单片机…

【云计算】银行数据中心私有云平台2.0建设(来自真实案例,很有启发性)

【导读】某行数据中心私有云平台一期建设后投入使用。但在使用过程中遇到了诸多实际问题:审批流程不贴合实际情况、自动化程度较低、云平台无法与CMDB联动、裸金属纳管等。本文对问题根源进行了探讨,并分享了通过对资源管理模式、审批流程、资源部署、微服务部署等方面进行优…

markdown学习记录

markdown学习 标题 标题用“#” 字体 这是加粗(两个星号) 这是倾斜(一个星号) 加粗+倾斜(三个星号) 这是删除线 (两个~~) 引用大于号是引用分割线(“---”或“***”) 插入图片 !+[名称]+(URL)超链接 [地址名]+(网址) 我的博客地址 列表 有序用数字,无序用“-”号A…

【实用技巧】【探讨】Java 中比较两个对象的差异

1 前言 大家平时写业务代码的时候,应该能感知到哪些是基础配置数据,哪些是实例数据。比如营销里的活动信息、促销信息就属于配置型数据,基于活动带来的订单参与活动信息属于实例数据。比如一些规则信息、流程信息等类似一种版本的概念。那么版本跟版本之间的差异、以及创建…

主机与虚拟机互ping不通的解决办法

一、主机ping不通虚拟机,但虚拟机可以ping主机 解决办法:二、虚拟机ping不通主机,但主机可以ping虚拟机 解决办法: 这里说的第一点我不知道,我是虚拟机可以ping通百度,之前虚拟机有过相关配置 我这里主要是win11电脑网络连接这里开启防火墙下面简单的说明下Vmware的网络连…

Win32汇编学习笔记09.SEH和反调试

Win32汇编学习笔记09.SEH和反调试-C/C++基础-断点社区-专业的老牌游戏安全技术交流社区 - BpSend.net SEH - structed exception handler 结构化异常处理 跟筛选一样都是用来处理异常的,但不同的是 筛选器是整个进程最终处理异常的函数,但无法做到比较精细的去处理异常(例如处理…

【Windows攻防】Windows注册表 IFEO注入

介绍 IFEO 注入攻击是一种基于注册表的攻击技术,几乎可以保证以登录用户甚至管理员/系统用户的身份执行代码。在任何你可以想象的用例中,如果你想要将你的有效载荷绑定到 Windows 平台上二进制文件的“启动”,IFEO(图像文件执行选项)可能是你最好的选择。这是一种开发人员…

[Java] 计算Java对象大小

序在Java应用程序的性能优化场景中,时常需要考虑Java对象的大小,以便评估后,进一步提出优化方案:占用内存的大小。(比如 本地内存) 对象数据在网络传输中占用的网络带宽 对象数据在存储时占用的磁盘空间 ...概述 对象大小如何计算对象大小包括俩部分的内容,对象头和对象…

并行前缀(Parallel Prefix)加法器

并行前缀(Parallel Prefix)加法器 并行前缀加法器的基本介绍 二进制加法器是目前数字计算单元中的重要模块,基础的加法器架构包括行波进位加法器(Ripple Carry Adder),超前进位加法器(Carry Look-Ahead Adder),进位选择加法器(Carry Select Adder)等。加法器的进位传…