第9章 常用的设计模式

第9章 常用的设计模式

9.1 聚合组件(aggregate component)

聚合组件将多个 底层 类型集中到一个 简化的高层 类型中,用于支持常用场景。例如,E-mail 组件,把简单邮件传输协议(SMTP)、套接字、编码(encoding)等等集中在一起。

聚合组件的目的是提供一个 更高层的抽象 ,便于开发者完成(通常很简单的)工作,不仅仅是提供另一种做事情的方式。

一个简单的例子:

string[] lines = File.ReadAllLines(@"c:\foo.txt");

聚合组件应提供所有默认实现,默认参数,以 降低用户使用门槛

9.1.1 面向组件的设计(component-oriented design)

面向组件的设计:一种设计方法,通过类型的构造函数、属性、方法及事件定义 API。它的设计要从使用模式出发:① 用 默认/相对简单的构造函数 创建一个实例;② 通过 属性 配置参数;③ 调用方法。这种使用模式被称为“ Create-Set-Call ”。

一个简单的例子:

// Create
var queue = new MessageQueue();// Set
queue.Path = queuePath;
queue.EncryptionRequired = EncryptionRequired.Body;
queue.Formatter = new BinaryMessageFormatter();// Call
queue.Send("Hello World");
queue.Close();

调用方法(Call)时,如果实例工作状态无效,应通过抛出 InvalidOperationException ​ 异常告知用户属性未被正常设置(Set)。

聚合组件是一个 facade,它基于面向组件的设计,并满足下列条件:

  • 默认构造函数:聚合组件应该有默认构造函数,便于 创建实例
  • 非默认构造函数:非默认构造函数的所有参数应该与 属性 相对应,并用来对 属性 进行初始化,以保证 属性 的一致性和完整性。
  • 属性:大多数属性应该有 getter 和 setter ,并且所有属性都有合理的默认值。
    避免空值或无效值的出现。
  • 方法:如果在(主要场景的)方法调用之间 参数不会改变 ,那么方法就不应该带这样的参数。这样的参数应该通过属性来指定。
    这样可以简化方法的签名和调用。
  • 事件:方法不以 委托 为参数。所有回调函数都通过 事件 来实现。

9.1.2 因子类型(factored type)

因子类型:聚合组件的 内部类型 ,它不应该有 工作状态 (即不存在无效状态),且有非常明确的 生命周期 。它可以通过聚合组件的 属性方法 进行访问。换句话说,他是聚合组件的参数类型。

9.1.3 聚合组件规范

  • CONSIDER​:考虑为 常用的功能 领域提供聚合组件。

  • DO​:聚合组件应该对 高层概念(物理对象) 建模,而非 系统级任务 建模。

    例如,组件应该对文件、目录、驱动器建模,不应该对流(stream)、格式化器(formatter)、比较器(comparer)进行建模。

  • DO​:通过 响亮的名字 提高聚合组件的可见性。

    名字要和系统中的实体相对应,比如 MessageQueue、Process、EventLog。这样的类型名更引人注目。

  • DO​:聚合组件要支持 Create-Set-Call 使用模式,且每个步骤都要尽可能地简单,不要让用户在一个场景中显式地实例化多个对象。

    如果开发者使用方式不符合要求,通过抛出异常告知用户应该怎么做。

聚合组件的 Create-Set-Call 的设计要遵循如下准则:

  • Create​ 要提供 默认 构造函数或 非常简单的 构造函数,也要提供有参构造函数用于设置 属性

  • Set​ 要提供可读写的属性,便于用户对属性 二次设置

  • Call ​要使用 事件 ,而非 委托 ;优先使用 事件 ,而非需要被覆盖的虚函数。

    • 聚合组件的目的是易用, 事件委托 更易于使用。

常用场景中则要求:

  • DON'T ​: 不可要求 用户使用继承、覆盖方法及实现接口。

    组件应该主要靠 属性属性 的组合来改变自己的行为。

  • DON'T​:不可要求用户进行 额外工作

    例如,让用户用配置文件来配置组件、让用户生成资源文件等。

  • CONSIDER​​:让聚合组件能够 自动 切换状态。

    • 例如,MessageQueue​ 实例既可以发送消息,也可以接受消息,但不应该让用户感受到模式切换的存在。
  • DON'T​:不要设计有 多种状态 的因子类型

    因子类型在整个生命周期内应该只有一种状态。例如,Stream 的实例表示一个已经打开的流,要么只能读取,要么只能写入。

  • CONSIDER​:将聚合组件集成到 Visual Studio 的 设计器 中。

    通过集成可以把组件放到 Visual Studio 的工具箱中,从而增加对拖放、属性网格(property grid)、事件挂接(event hookup)等支持。
    只需实现 IComponent​ 接口,或继承实现了该接口的类型,如 Component​ 或 Control​,组件便可以与 Visual Studio 集成。

  • CONSIDER​:考虑将聚合组件和因子类型放在 不同 的程序集中。

    这样组件不会被单一因子类型限制,且不会导致循环依赖性。

    该规则要求:因子类型实现相应接口,组件通过接口调用因子类型,该接口应该放在第三方程序集中,避免循环依赖。

  • CONSIDER:建议 把聚合组件内部的因子类型暴漏给外界访问。

    因子类型最适于把不同特性域集成在一起。例如,SerialPort​ ​组件向外界暴漏了它在内部使用的流,这样用户就可以将该组件与其他的可重用 API(比如对流进行压缩的 API)结合起来使用。

9.2 异步模式

.NET 中有三种方式实现异步:

  • 经典 异步模式
  • 基于 事件 的异步模式
  • 基于 任务 的异步模式

9.2.1 选择异步模式

  • DO​​​:要使用基于 任务 的异步模式来实现新的异步 API。

  • CONSIDER​​:建议将经典异步模式或事件异步模式 API 升级为基于 任务 的异步模式。

    我们可以使用 TaskFactory ​ 和 TaskFacotry<TResult> ​ 类型包装现有的经典异步模式和基于事件的异步模式 API。

9.2.2 基于任务的异步模式

  • DO​​​:异步方法的命名要使用 Async ​​​后缀。

  • CONSIDER​​​​:建议为异步方法添加 同步 变体。

    // 异步方法
    public Task<ParsedData> ParseAsync(string filename, CancellationToken = default) { ... }
    // 同步变体
    public ParsedData Parse(string filename) { ... }
    

  • DO​:异步方法要接收 CancellationToken ​参数,并命名为“ cancellationToken ”,且提供默认值。它最好作为 最后 一个参数,这样可以更好地与同步方法对齐。

    // 错误,调用者无法取消
    public Task WriteAsync(string text) { ... }
    // 正确,支持CancellationToken作为可选参数
    public Task WriteAync(string text, CancellationToken cancellationToken = default) {...
    }// 错误,CancellationToken应该作为最后一个参数,即使它没有默认参数
    public Task WriteAync(string text, CancellationToken cancellationToken, Encoding encoding) {...
    }
    

  • CONSIDER​:需要长时间运行或阻塞的同步方法建议接收 CancellationToken ​ 参数。但该方法不应该返回 Task​。

    CancellationToken​ 类型与异步执行或 Task 无关,它同样适用于那些 超时 、由用户 发出中止信号 的同步(阻塞)方法。最常见的是等待线程同步和 I/O 操作,如下载文件。

    此外,该方法不是异步方法,不应该返回 Task ​,也不应该使用 Async 后缀。

  • DON'T​:不要在异步方法中使用 refout 参数修饰符。

    .NET 运行时只允许栈上的值引用(即方法参数或本地变量),异步方法需要让渡当前执行,无法继续维持值引用。

    对于标注了 async 的方法,C#编译器可以发现 ref 和 out 的错误使用,但未标注 async 的 Task(异步)方法,不会报错。

  • DON'T​:不要在 虚的(抽象) 的异步方法中使用 in 参数修饰符。

  • AVOID​:避免在 非虚 的异步方法中使用 in 参数修饰符。

    与 ref、out 相同,标注了 async 的方法使用 in 参数修饰符 C# 编译器会报错,未标注 async 的异步方法不会报错。

    在模板方法模式中,模板方法(非虚)会调用虚方法,而虚方法禁止使用 in 参数修饰符(见上一条),即使模板方法用了 in,向虚方法传参时仍会发生 拷贝,脱离了预期。

    // 模板方法(非虚),使用in修饰参数
    public Task TestMethodAsync(in int value){// 此时传入的value发生了值拷贝TestMethodAsyncCore(value);
    }
    // 虚方法,未使用in修饰参数
    protected abstract Task TestMethodAsyncCore(int value);// 这里进行调用
    int value = 0;
    var task = TestMethodAsync(int value)
    // 此处我期望通过修改值改变TestMethodAsync的行为,但已发生了值拷贝,不会符合预期
    value = 1;
    await task;
    

    当然,不像 ref 和 out,in 修饰的参数不能被修改,且非虚方法不会覆写,不存在混淆的可能性,所以可以酌情使用 in 修饰符。

9.2.3 异步方法的返回类型

  • DO​​​:不具有返回值的异步方法,要使用 Task ​​​(非泛型)作为返回类型,而非 ValueTask ​​​。

    Task ​比 ValueTask ​有更好的可用性,如 Task.WaitAny(Task[])​ 不支持 ValueTask ​,对于标注了 async 的方法,两者性能相近

  • DO​​:具有返回值的异步方法,要使用 Task<TResult> ​​作为返回类型,而非 ValueTask<TResult> ​​。

    相比节省的内存,可用性更重要。

  • CONSIDER​:如果你的异步方法通常是同步完成的,建议使用 ValueTask<TResult> ​。

    例如,System.IO.Stream ​的 ReadAsync ​方法在调用时,由于缓冲的存在,该方法经常同步返回,且该方法返回的是 Int32​。如果使用 Task<TResult>​,在循环调用时会频繁创建 Task<Int32>​ 实例,它产生的内存影响可能会很明显。此时使用 ValueTask<TResult>​ 更为合适。

    上述情况仅适用于同步完成,如果该方法(标识为 async)未能同步完成,将会创建一个新的 Task<TResult>​ 实例进行跟踪剩余部分,反而导致性能下降。

9.2.4 为现有的同步方法制作一个异步变体

  • DO​:同一个方法的同步版本和异步版本,其参数顺序尽可能保持相同。

    若同步方法没有 ref、out 参数,两者的差别应该仅有 CancellationToken ​ 参数和 Task ​返回值:

    public partial class XDocument {public static XDocument Load(TextReader textReader, LoadOptions options) { ... }public static Task<XDocument> LoadAsync(TextReader textReader, LoadOptions options, CancellationToken cancellationToken = default) { ... }
    }
    

  • DO​:如果同步方法有 refout 参数,适当调整异步方法的参数,以匹配同步方法。
    例如,返回值为 void 的同步方法有一个 out 参数作为返回值:

    // 同步方法
    public void CalculateValue(int input, out int result) { ... }
    // 异步方法
    public Task<int> CalculateValueAsync(int input, CancellationToken cancellationToken = default) { ... }
    

  • CONSIDER​:建议在异步方法中使用 步回调代替 步回调,或通过 重载 同时接收两者。

    // 同步方法
    public void DoWork(Action<State> callback) { ... }
    // 具有同步回调的异步变体
    public void DoWorkAsync(Action<State> callback,CancellationToken cancellationToken = default) { ... }
    // 具有异步回调的异步变体
    public void DoWorkAsync(Func<State, CancellationToken, Task> asyncCallback,CancellationToken cancellationToken = default) { ... }
    

9.2.5 异步模式一致性的实现准则

  • DON'T ​​: 不要Task​ 或 Task<TResult>​ 返回 null。

    为什么要返回 null?是因为无法执行还是执行结果有误?无论如何, 抛出异常 都是更好的方案。

9.2.5.1 Task.Status​ 的一致性

  • DON'T​:不要返回处于 Created 状态的 Task​。

    Task​ 构造函数创建实例时,该实例的 Status​ 为 Created (即未启动),此时我们可以设置该 Task​。但是,如果不启动该 task,直接进行 wait,该 wait 将永远不会结束。
    通过 async​、Task.Run()​ 和 Task.Factory.StartNew()​ 创建任务,则会在启动后才返回 Task​。

  • DO​:当任务因 CancellationToken​ 中断,要抛出 OperationCanceledException ​。

    抛出该异常,会使 Task.Status​ 处于 Canceled 状态。

    try{await SaveData(data, cancellationToken);QueueNotification("Data saved successfully!");
    } catch (OperationCanceledException){QueueNotification("Save was canceled.");
    }
    ...
    private async Task SaveData(byte[] data, CancellationToken cancellationToken) {// 错误:这会产生任务成功的消息if (cancellationToken.IsCancellationRequested) {return;}// 正确:这会为调用者提供任务已取消的消息if (cancellationToken.IsCancellationRequested) {throw new OperationCanceledException(cancellationToken);}// 正确:这会为调用者提供任务已取消的消息cancellationToken.ThrowIfCancellationRequested();...
    }
    

9.2.5.2 等待正确的上下文

  • DO ​​:在等待一个异步操作时, 使用 await task.ConfigureAwait(false)​​,除非是在 依赖同步上下文的 应用程序模型中。

9.2.5.3 避免死锁

  • DON'T ​: 不要 在异步方法中调用 Task.Wait()​ 或读取 Task.Result​ 属性;相反, 使用 await。

    Task.Wait()​ 方法和 Task.Result​ 属性是阻塞性的,在某些情况下会导致死锁。

  • DO​​:要在异步方法的实现中调用 步方法变体,而不是 步方法变体。

    如果一个方法既有同步变体又有异步变体,那么同步方法有可能导致阻塞、资源耗尽和死锁。

9.2.5.4 正确处理 ValueTask​ 和 ValueTask<TResult>

  • DON'T ​: 禁止ValueTask​ 或 ValueTask<TResult>​ 的实例进行一次以上的操作,或者保存它;只应该 await 或返回它。

    简而言之:永远不要有 ValueTask​ 或 ValueTask<TResult>​ 局部变量。如果你需要该 Task​,可以用 AsTask() ​ 方法获取相应的 Task​ 对象:

    Task<int> task = SomeMethodAsync(...).AsTask();
    

9.2.5.5 异步方法的异常

  • DO​​​​:三种异常中的 使用错误 异常,要直接从异步方法中抛出。
    使用错误 异常可以在 Task 运行前进行校验、抛出,这样异常不会被包装在 Task 中,有以下好处:

    • 调用栈可以清晰地显式错误来源;
    • 即使不等待(Task.Wait),也不会忽略异常;
    • 若输入无效,则任务不会执行。

  • DO​:三种异常中的 执行错误 异常,要从异步方法返回的 Task​ 值中抛出。

9.2.9 IAsyncEnumerable<T>

IAsyncEnumerable<T> ​接口主要用于生成 异步迭代器 ,用于 C#8.0 的 await foreach 语句。

  • DO​:要为返回 IAsyncEnumerable<T>​ 的方法使用“ Async ”后缀。

  • DO​:使用了 yield returnIAsyncEnumerable<T>​ 方法,其 CancellationToken​ 形参要应用 [EnumeratorCancellation] ​ 特性。

    否则 GetAsyncEnumerator​ 的 CancellationToken​ 值在编译时会被忽略

    public static async IAsyncEnumerable<int> ValueGenerator(int start,int count,[EnumeratorCancellation] CancellationToken cancellationToken) {int end = start + count;for (int i = start; i < end; i++){await Task.Delay(i, cancellationToken).ConfigureAwait(false);yield return i;}
    }
    

  • DON'T​:除非是作为 GetAsyncEnumerator ​ 方法的返回类型,否则不要使用 IAsyncEnumerator<T>​ 及其派生类型。

  • DON'T:不要 为同一个公开类型同时实现 IAsyncEnumerator<T>​​​和 IAsyncEnumerable<T>​​​。

9.2.10 await foreach 的使用准则

  • DO​:await foreach 作用于 IAsyncEnumerable<T>​ 时,要使用 WithCancellation修改器 ,以便在枚举器中使用 CancellationToken​。

    // 如下,cancellationToken被添加了特性标记
    public Task<int> MaxAsync(IAsyncEnumerable<int> source,CancellationToken cancellation = default) {int max = int.MinValue;bool hasValue = false;await foreach (int value in source.WithCancellation(cancellationToken).ConfigureAwait(false)) {hasValue = true;if (value > max){max = value;}}return hasValue ? max : throw new InvalidOperationException();
    }// 如果是如下这种形式,则不需要使用WithCancellation修改器了
    await foreach (int value inValueGenerator(10, 5, cancellationToken).ConfigureAwait(false)) {...
    }
    

  • DO​​​:在使用 await foreach 时要使用 ConfigureAwait ​ ​修改器,与使用 await 时相同。

    示例代码见上一条准则。

9.3 依赖属性

  • DO​:如果你需要属性来支持 WPF 的功能,如样式、触发器、数据绑定、动画、动态资源和继承,则要提供 依赖 属性。

9.3.1 依赖属性设计

  • DO​:在实现依赖属性时,要继承 DependencyObject ​ 或其子类。

    该类型提供了一个非常高效的属性存储实现,并自动支持 WPF 数据绑定。

  • DO​:要提供一个常规的 CLR 属性和公开的静态 只读 字段,为每个依赖属性存储一个 System.Windows.DependencyProperty ​ 的实例。

    public class TextButton : DependencyObject {public string Text{get => (string)GetValue(TextProperty);set => SetValue(TextProperty, value);}public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(TextButton));
    }
    

  • DO​:要通过调用 DependencyObject.GetValue() ​ 和 DependencyObject.SetValue() ​ 实例方法来实现依赖属性。

    public class TextButton : DependencyObject {public string Text{get => (string)GetValue(TextProperty);set => SetValue(TextProperty, value);}public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(TextButton));
    }
    

  • DO​:在命名依赖属性的静态字段时,要以属性名称加上“ Property ”后缀作为其名称。

    DependencyProperty.Register()​ 方法的第一个参数应该是属性名称。

  • DON'T​:不要在代码中显式地为依赖项属性设置默认值;相应的,应该在 元数据PropertyMetadata ​)中进行设置。

    显式设置的默认值极有可能无法通过某些隐式方式进行设置(比如样式)

    public class TextButton : DependencyObject
    {public TextButton(){// 不要显式设置依赖属性的默认值Text = String.Empty; // 错误做法}public static readonly DependencyProperty TextProperty =DependencyProperty.Register("Text", typeof(string), typeof(TextButton),new PropertyMetadata(String.Empty)); // 正确做法
    }
    

  • DON'T​:除了访问静态字段的标准代码,不要将其他代码放在 属性的访问 器中。

    如果该属性是通过隐式方式设置的(比如样式,它会绕过属性访问器,直接访问静态只读的 DependencyProperty​),这些“其他代码”就不会执行。

    public string Text
    {get => (string)GetValue(TextProperty);set{SetValue(TextProperty, value);DoWorkOnTextChanged(); // 错误做法 }
    }
    

  • DON'T​:不要使用依赖属性来存储与 安全 相关的数据,即使是私有的依赖属性也可以被 公开 访问。

    public class BadType : DependencyObject
    {// 切勿这么做,这不安全private static readonly DependencyProperty SecretProperty =DependencyProperty.Register( "Secret", typeof(string), typeof(BadType));public BadType(){SetValue(SecretProperty, "password");}private static void Main(){var b = new BadType();var enumerator = b.GetLocalValueEnumerator();while (enumerator.MoveNext()){Console.WriteLine("{0}={1}", enumerator.Current.Property, enumerator.Current.Value);}}
    }
    

9.3.2 附加属性的设计

依赖属性的准则同样适用于附加属性,但附加属性还有一些额外的准则。

  • DO​:附加属性的定义方式和常规的依赖属性看起来非常相似,除了访问器是 静态Get*()​ 和 Set*()​ 方法。

    public class Grid
    {public static int GetColumn(DependencyObject obj){return (int)obj.GetValue(ColumnProperty);}public static void SetColumn(DependencyObject obj, int value){obj.SetValue(ColumnProperty, value);}public static readonly DependencyProperty ColumnProperty =DependencyProperty.RegisterAttached("Column", typeof(int), typeof(Grid));
    }
    

9.3.3 依赖属性校验

  • DON'T​:不要在依赖属性的访问器中添加任何校验逻辑。相反,应该在 DependencyProperty.Register() ​ 方法中提供 校验回调

    // 错误做法
    public string Text {get { ... }set {if (value == null)throw new ArgumentNullException(nameof(value));...}
    }
    
    // 正确做法
    public static readonly DependencyProperty TextProperty =DependencyProperty.Register("Text", typeof(string), typeof(TextButton),new PropertyMetadata(string.Empty), value => value != null); // 校验
    

9.3.4 依赖属性变更通知

  • DON'T​:不要在依赖属性的访问器中实现变更通知逻辑。依赖属性有内置的变更通知功能,可通过向 PropertyMetadata ​ 传入回调进行变更通知。

    public static readonly DependencyProperty TextProperty =DependencyProperty.Register("Text", typeof(string), typeof(TextButton),new PropertyMetadata(string.Empty,(obj, args) => {// property changed...}));
    

9.3.5 依赖属性中的值强制

什么是“值强制”(Value Coercion)

给定的值因不符合要求,在属性内部被修改,这种行为称为“属性强制”。下面是一个简单的强制逻辑:

public string Text {set {if (value == null) {value = string.Empty;}_text = value;}
}

在依赖属性中,上述变化由回调函数完成,而非在属性设置器中进行。

约定

  • DON'T​:不要在依赖属性的设置器中实现强制逻辑。依赖属性有内置的强制功能,可通过向 PropertyMetadata ​ 传入回调进行值强制。

    public static readonly DependencyProperty TextProperty =DependencyProperty.Register("Text", typeof(string), typeof(TextButton),new PropertyMetadata(string.Empty,(obj, args) => { /* change notification callback */ },(obj, value) => value ?? string.Empty // coercion));
    

9.4 Dispose 模式

  • DO​:要为包含 可处置(Dispose)类型实例的类型 实现基本的 Dispose 模式。

  • CONSIDER​​​:考虑为那些本身不持有,但其派生类可能会持有非托管资源或可处置对象的类,实现基本 Dispose 模式。

    例如:System.IO.Stream​ 类,它本身是一个不持有任何资源的抽象基类,但它的大多数子类都持有资源,因此,它实现了 Dispose 模式。

9.4.1 基本 Dispose 模式

  • DO​​:要声明一个受保护的 虚方法 void Dispose(bool disposing) ​​​,用于集中释放资源有关的代码。

    该方法被终结器(析构函数)和 IDisposable.Dispose​ ​调用。

    protected virtual void Dispose(bool disposing) {if(disposing) {_resource?.Dispose();}
    }
    

    其 bool 参数用于标记是被显式调用(IDisposable.Dispose​ 调用)还是隐式调用(终结器调用)。被显式调用时,实例参数仍可使用;反之则不可。

  • DO​:要通过简单地调用 Dispose(true) ​和 GC.SuppressFinalize(this) ​来实现 IDisposable ​接口的 Dispose() ​方法。

    public void Dispose() {Dispose(true);GC.SuppressFinalize(this);
    }
    

    GC.SuppressFinalize(this)​ 会抑制终结器触发,如果 Dispose(true)​ 抛出了异常,GC.SuppressFinalize(this)​ 未执行,则 GC 仍会调用终结器。

  • DON'T ​: 不要 将无参数的 Dispose​ 方法实现为虚方法。

    错误的设计
    public class DisposableResourceHolder : IDisposable {public virtual void Dispose() { ... }protected virtual void Dispose(bool disposing) { ... }
    }正确的设计
    public class DisposableResourceHolder : IDisposable {public void Dispose() { ... }protected virtual void Dispose(bool disposing) { ... }
    }
    

  • DON'T​​:除了 Dispose() ​和 Dispose(bool) ​,不要重载其他 Dispose ​方法。

  • DO:要允许 Dispose(bool)​ 方法被多次调用,该方法可以选择在第一次调用之后 就什么都不做了

    • public class DisposableResourceHolder : IDisposable {bool _disposed = false;protected virtual void Dispose(bool disposing) {if (_disposed) {return;}// 清理...disposed = true;}
      }
      

  • AVOID ​: 避免Dispose(bool)​ 中抛出异常,除非 是在进程被破坏的关键情况下

    如内存泄漏、不一致的共享状态等。

    在用户的预期中,调用 Dispose​ ​方法不会抛出任何异常。尤其不要在 Dispose(false)​ ​时抛出异常(即终结器释放资源),否则会终止进程。

  • DO​:如果任何成员在 Dispose 后无法继续使用,则被调用时要抛出 ObjectDisposedException ​ 异常。

    public class DisposableResourceHolder : IDisposable
    {bool _disposed;SafeHandle _resource;protected virtual void Dispose(bool disposing){if(_disposed)return;// 清理资源..._disposed = true;}public void DoSomething(){if(_disposed){throw new ObjectDisposedException(...);}}
    }
    

  • AVOID​​​:在调用 Dispose() ​方法后,要避免对象 重新获得有意义 的状态。

    使对象重写获取有意义的状态被称为“rehydration”。它会造成难以诊断的性能问题,特别是当对象重要的待释放部分是被惰性创建时,很容易产生 bug。

  • CONSIDER​:如果“ Close ”是该领域的标准术语,那么除了 Dispose() ​方法,还要提供一个 Close() ​方法。

9.4.2 可终结类型

  • DON'T​:不要让 公开 类型成为可终结类型。

    • 可终结资源的持有者应该是内部(Internal)或私有嵌套类型。
    • 要优先使用现成的资源包装器,如 SafeHandle。

    Notice

    这里说的是“可终结”,而非“可释放”!

  • DO​:在每个可终结类型上实现基本 Dispose 模式

    这样调用者可以根据自己的需要释放由终结器释放的资源。

  • DON'T​:不要访问终结器代码路径中 任何可终结的对象 ,因为有很大的风险,它们可能已经 被终结了

  • DON'T​:不要在终结器逻辑中 抛出异常 ,除非是系统关键性的故障。

  • DON'T ​:如果非密封的公开类型(可以作为基类)具有终结器, 不要 移除它的终结器。

    即“非密封”若通过基本的 Dispose 模式实现了终结器,其派生类可能依靠它释放资源,切勿删除该终结器,以免内存泄漏;

    而“密封”类型不能进行派生,删除终结器的后果是可控的;如果是内部类(internal),我们可以检查所有派生类后再移除终结器。

9.4.3 限定作用域的操作

  • CONSIDER​:考虑返回一个 IDisposable ​ 实例,而不是让调用者手动管理“ 开始 ”方法和“ 结束 ”方法。

    我们可以使用 using 语句,将逻辑作用域和方法作用域对齐,调用者可以不再提供与“ 开始 ”方法相对应的“ 结束 ”方法。

    // 我们经常这么使用using
    using (var reader = File.OpenRead(path)){...
    }
    // 其实还可以这么用。这里using语句仍会把File.OpenRead的返回值释放。
    // 此处只是范例,具体的示例代码见P294,更有实用性。
    using(File.OpenRead(path)){...
    }
    

  • CONSIDER​:为了程序的正确性,当“结束”方法必须位于 finally 块中时,考虑返回一个 IDisposable 实例,而不是让调用者手动管理“ 开始 ”方法和“ 结束 ”方法。

    • 如下代码 _lock.GetReadLock ​返回了一个 IDisposable ​实例:

      public void DoStuff() {using(_lock.GetReadLock()){DoStuffCore();}
      }
      

9.4.4 IAsyncDisposable

​#delay#​这部分内容暂不进行学习,我没搞懂是干啥的

  • DO​:在实现 IAsyncDisposable ​时,除非另有说明,否则应遵循异步设计同步 Dispose 中的准则。

  • DO​:不要为 DisposeAsync() 声明任何重载。

    同步 Dispose() ​方法有两个重载方法(Dispose() ​和 Dispose(bool)​),其中 Dispose(bool) ​方法被 Dispose() ​和终结器调用,因此有重载。而 DisposeAsync() ​不会被终结器调用,因此无需重载。

  • DO​:DisposeAsync ​的实现方式如下:

    • 调用并等待 DiposeAsyncCore()​;
    • 调用 Dispose(false)​;
    • 调用 GC.SuppressFinalize(this)。​

    代码如下:

    public partial class SomeType : IDisposable, IAsyncDisposable
    {protected virtual void Dispose(bool disposing) { ... }public void Dispose(){Dispose(true);GC.SuppressFinalize(this);}public async ValueTask DisposeAsync(){await DisposeAsyncCore();Dispose(false);GC.SuppressFinalize(this);}protected virtual ValueTask DisposeAsyncCore() { ... }
    }
    

await using 的使用准则

  • DO​:在使用 await using ​时,要使用 ConfigureAwait ​修改器,这与使用 await ​的方式相同。

9.5 工厂

  • DO​:优先使用 构造函数 ,而非 工厂

    构造函数的可用性、一致性、易用性更好。工厂为了灵活性,牺牲了可发现性、可用性、一致性。

  • CONSIDER​:如果需要对创建实例的 过程进行 控制,可以考虑使用工厂。

    工厂方法可以轻松的实现执行对象缓存、节流和共享。

  • DO​:在开发者不知道构建哪种类型的情况下使用 工厂

    可以通过传入参数、上下文等方式,令工厂返回指定用途的实例:

    public class Type
    {// 该工厂方法返回了大量类型的实例,// 包括 PropertyInfo、ConstructorInfo、MethodInfo 等public MemberInfo[] GetMember(string name);
    }
    

  • CONSIDER​:如果工厂的 语义化 表达比构造函数好,考虑使用工厂。

    构造函数不能有名称,而且有时候缺乏足够的上下文信息告知开发者一个操作的语义,如:

    // 无法体现意图
    public String(char c, int count);// better
    public static String Repeat(char c, int count);
    

  • DO​:转换式操作应使用 工厂 ,如 Parse ​或 Decode​。

  • DO​:工厂 实现为方法,而 属性。

  • DO​:工厂创建的实例方法应通过 返回值 返回,而非 out 参数

    也有例外,如 Try-Parse 模式。

  • CONSIDER​:当工厂方法创建的是另一个类的实例,考虑将 Create 和 所创建的类型名拼接在一起来 命名工厂方法。

    如:创建按钮的工厂方法命名为 CreateButton ​。

    在某些情况下,可以使用特定领域的名称,如:File.Open​。

  • CONSIDER​:通过将所创建的类型的名称和 Factory 拼接到一起来 命名工厂类型。

    如:创建 Control 对象的工厂命名为 ControlFactory ​。

9.6 LINQ 支持

9.6.2 实现 LINQ 支持的方法

9.6.2.1 通过 IEnumerable<T>​ 支持 LINQ

  • DO​:要通过实现 IEnumerable<T> ​ 接口来启用基本的 LINQ 支持

  • CONSIDER​:考虑实现 ICollection<T> ​ 来提升 LINQ 查询的性能

    Enumerable.Cout()​ 方法默认通过迭代的方式统计数量。若集合实现了该接口,该方法会返回 ICollection<T>.Count ​ 属性值,时间复杂度可以从 \(O(n)\) 降低为 \(O(1)\)

  • CONSIDER​:可以在 当前集合类型 上自定义特定的 LINQ 方法,以覆写(覆盖)默认的 System.Linq.Enumerable​ 实现(例如为了优化性能)

    当然也可以通过自定义扩展方法覆写默认的 LINQ 方法。例如 ImmutableArrayExtensions.Select()​ 扩展方法提供了性能更好的 Select()​:

    public partial static class ImmutableArrayExtensions
    {public static IEnumerable<TResult> Select<T, TResult>(this ImmutableArray<T> immutableArray, Func<T, TResult> selector){ ... }
    }
    

9.6.2.2 通过 IQueryable<T>​ 支持 LINQ

  • CONSIDER​:若需要使用查询表达式,考虑实现 IQueryable<T>

  • DON'T​:若不清楚 IQueryable<T>​ 带来的 性能 影响,不要实现它

    构建和解释表达式树的开销非常大,IQueryable<T>​ 的执行实际很慢。

    不过这在 LINQ to SQL 中是完全可以接受的,表达式与 SQL 语句的转换等一系列的开销远小于数据库实际查询的开销

  • DO​:如果从逻辑上说某个接口方法不能被数据源支持,应该从 IQueryable<T>​ 方法中抛出 NotSupportedException ​ 异常。

    譬如某个媒体流所表示的 IQueryable<byte>.Cout()​ 方法在逻辑是不被支持(流可以被认为是无限的),此时 Count()​ 方法就应该抛出上述异常。

9.6.2.3 通过查询模式支持 LINQ

查询模式

查询模式指:因没有实现 IQueryable<T>​/IEnumerable<T>​/ICollection<T>​ 等接口,开发者无法使用 LINQ,此时可以自定义 方法 / 扩展方法 ,以支持形如 LINQ 的数据查询。

查询模式的方法签名有:

S<T> Where<T>(this S<T>, Func<T, bool>)S<T2> Select(this S<T1>, Func<T1, T2>)
S<T3> SelectMany(this S<T1>, Func<T1, S<T2>>, Func<T1, T2, T3>)
S<T2> SelectMany(this S<T1>, Func<T1, S<T2>>)O<T> OrderBy(this S<T>, Func<T, K>), where K is IComparable
O<T> ThenBy(this O<T>, Func<T, K>), where K is IComparableS<T> Union(this S<T>, S<T>)
S<T> Take(this S<T>, int)
S<T> Skip(this S<T>, int)
S<T> SkipWhile(this S<T>, Func<T, bool>)S<T3> Join(this S<T1>, S<T2>, Func<T1, K1>, Func<T2, K2>, Func<T1, T2, T3>)T ElementAt(this S<T>, int)

上述签名中, S<T> ​ 表示集合, O<T> ​ 表示 IOrderedEnumerable<T>​ 或其子类。如果上述方法是成员方法,应省去第一个参数,内部使用 this ​ 指针进行调用。

约定
  • DO​:如果该 LINQ 方法在 LINQ 之外对该类型也是有意义的,要使用 类型的实例成员 来实现查询模式,否则应将其实现为 扩展方法

    以如下两段代码为例,对于数据集合来说,第 种实现方式更为合理:

    public partial class MyDataSet<T> : IEnumerable<T> { ... }public static partial class MyDataSetQueries{public static int Count(this MyDataSet<T> data) { ... }
    }
    
    public partial class MyDataSet<T> : IEnumerable<T>
    {public int Count() { ... }
    }
    

  • DO​:要为实现查询模式的类型实现 IEnumerable<T> ​ 接口。

  • CONSIDER​:考虑所设计的 LINQ 运算符返回 特定的可枚举 实例,而非简单的 IEnumerable<T> ​ 实例。

    假设我们有一个自定义集合类型 MyType​,它内部定义了优化后的 Count()​ 方法,但是用 LINQ 的 Where()​ 方法查询后,将得到 IEnumerable<T>​ 实例,此后再进行 Count()​ 运算将调用 LINQ 的 Count()​ 方法,优化失效:

    var result = myInstance.Where(query).Count();
    

    对此我们应同步覆写 LINQ 的 Where()​ 等方法,使其仍返回 MyType​ 类型,保证优化持续生效。

  • AVOID​:如果不希望回退到基本的 IEnumerable<T> ​ 实现,要避免只实现查询模式的一部分。

    理由同上一条准则。

  • DO​:将有序序列表示为与其无序对应的独立类型。

    原因见下一条准则:

    • DO​:要为有序序列类型定义 ThenBy() ​ 方法,或为其实现 IOrderedEnumerable<T> 接口。

      LINQ 中的 ThenBy()​ 方法是为 IOrderedEnumerable<T> ​ 接口定义的.

  • DO​:要为有序序列类型定义 ThenBy() ​ 方法,或为其实现 IOrderedEnumerable<T> 接口。

    LINQ 中的 ThenBy()​ 方法是为 IOrderedEnumerable<T> ​ 接口定义的.

  • DO​:要推迟查询运算符实现的执行。

    即 LINQ 的延迟执行,见8.4 延迟执行。

    不过自定义的查询模式怎么能延迟执行,我就不了解了。

  • DO​:令查询扩展方法的命名空间为主空间的“ Linq ”子命名空间。

    例如 System.Data 功能的扩展方法位于 System.Data.Linq 命名空间中。

  • DO​:若需要对查询(Query)进行检查,要使用 Expression<Func<...>> ​ 作为参数,而非 Func<...>

    使用表达式可以:

    1. 使 Lambda 转为 SQL 表达式成为可能;

    2. 执行时可进行优化

      例如对有序列表来说,可以使用二分查找加快 Where()​ 的执行速度。

9.7 可选功能模式

  • CONSIDER​:考虑使用 可选功能 模式为抽象提供可选功能。

    该模式替代了 因子 设计导致的动态转换,减少了框架的复杂性,提高了可用性。

    如果只有一小部分派生类,甚至只有一个派生类,会用到该功能,则使用基于 接口 的设计更好。

    另外,当可选功能的组合数量较少,且因子化提供的编译时安全性又很重要, 因子 设计是首选。

  • DO​:要提供一个简单的返回 布尔 类型的属性说明当前派生类是否支持某个可选功能。

    public abstract class Stream
    {public virtual bool CanSeek { get { return false; } }public virtual void Seek(int position) { ... }
    }
    

  • DO​:要在基类上为定义可选功能的虚方法抛出 NotSupportedException 异常。

    public abstract class Stream
    {public virtual bool CanSeek { get { return false; } }public virtual void Seek(int position) {throw new NotSupportedException(...);}
    }
    

9.8 协变和逆变

​#todo#​这部分内容待看完《深入理解 C#》再回头看

9.9 模板方法

该模式的目标是控制扩展性。

  • AVOID ​:公开成员 要避免 成为虚成员。

    如果要设计虚成员,应遵循模板方法模式:创建一个 受保护的虚 成员,由 公开 成员调用,这种做法提供了更可控的扩展性。

    public class Control{public void SetBounds(int x, int y, int width, int height){...SetBoundsCore(...);}public void SetBounds(int x, int y, int width, int height, BoundsSpecified specified){...SetBoundsCore(...);}protected virtual void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified){// 真正的处理逻辑。}
    }
    

  • CONSIDER​:考虑使用 模板方法模式 提供更可控的可扩展性。

    该模式下,所有扩展的功能点都是通过受保护的虚成员提供的,被非虚成员调用。

  • CONSIDER​:模板方法模式中的 protected virtual ​方法应使用 非虚方法名 + Core ​后缀。

  • DO​:模板方法中,非虚成员在调用虚成员前,要 执行参数状态校验

  • DO​:模板方法中,虚成员仅需执行 派生类特有 的参数和状态校验。

    也就是说,不要重复执行已经在非虚成员中做过的校验。

    通过将普通的检查转移到公开成员中,派生类型的开发者只需要关注其特定实现的逻辑。

9.10 超时

  • DO​​: 优先选用 方法参数设置超时时间, 而非 属性。

    方法参数 和超时之间的关联更加明显。如果该类型用于 VS 组件,则 属性 更好。

  • DO​:优先选用 TimeSpan ​表示超时时间。

    过去,超时时间是由整数表示的,其有以下劣势:

    • 时间单位不明显;
    • 将时间单位转换为代码中常用的毫秒较为麻烦。

    如果满足下列条件,用整数也是可以接受的:

    • 参数或属性名描述了时间单位,如参数名为 milliseconds​;
    • 最常用的数值换算起来很容易,如:单位是毫秒,常用的超时时间小于 1 秒。

  • DO​:当达到设置的超时时间时,要抛出 System.TimeoutException ​异常。

    超时方法可以考虑实现如下两个逻辑:

    • 当超时参数设置为 TimeSpan(0)​,则意味着操作应立即完成,否则直接抛出异常;

    • 当超时参数设置为 TimeSpan.MaxValue​,则意味着操作应该永远等待,且不会发生超时。

      实际上大部分会使用 Timeout.Infinite​ 作为永远等待。

    如果超时并抛出了 System.TimeoutException​ 异常,服务器类应该取消背后的操作。

  • DON'T ​: 不要 返回错误码来表示超时异常。

9.11 XAML 可读类型

  • CONSIDER​:自定义类型若想在 XAML 中工作,考虑为它提供 无参 构造函数。

    例如,下面的 XAML 标记等价于右侧的 C# 代码:

    <Person Name="John" Age="22"/>
    
    new Person() { Name = "John", Age = 22 };
    

  • DO​:若想在 XAML 解析器中使用不可变类型,应当提供相应的 标记扩展

    不可变类型不能使用 XAML 记录器,因此要通过 标记扩展语法 使用。以如下不可变类型为例,应提供相应的 标记扩展 类:

    public class Person
    {public string Name { get; }public int Age { get; }public Person(string name, int age){Name = name;Age = age;}
    }
    
    [MarkupExtensionReturnType]
    public class PersonExtension : MarkupExtension
    {public string Name { get; set; }public int Age { get; set; }public override object ProvideValue(IServiceProvider serviceProvider){return new Person(Name, Age);}
    }
    

  • AVOID ​: 避免 定义新的类型转换器,除非该转换是自然且直观的。一般来说,应当只使用 .NET 内置的类型转换器。

    尤其是通过类型转换器定义了新的“迷你语言”,会大大增加系统的复杂性。

  • CONSIDER​:考虑应用 ContentPropertyAttribute ​ 为最常用的属性启用方便的 XAML 语法。

    关于该属性的用法,请参考机制 3:父元素使用 ContentProperty 特性进行修饰。

9.12 操作缓冲区

​#todo#​在程序考虑切换至.net 8 平台后再看

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

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

相关文章

深入浅出:Agent如何调用工具——从OpenAI Function Call到CrewAI框架

深入浅出:Agent如何调用工具——从OpenAI Function Call到CrewAI框架 嗨,大家好!作为一个喜欢折腾AI新技术的算法攻城狮,最近又学习了一些Agent工作流调用工具的文章,学完之后,我真的是“啊这”,一边感慨AI技术的强大,一边觉得自己打开了新世界的大门。于是,我决定写这…

在Ubantu中安装pycharm

1.下载pycharm linux版,我下载的是2022.3.3专业版 2. 更改host文件,输入: sudo gedit /etc/hosts在弹出的文件中的末尾加以下代码: 0.0.0.0 account.jetbrains.com3.激活pycharm: 将pycharm补丁jet-netfilter拷入ubantu中某一路径(注意是整个文件夹放进去,不要只放jar包…

docker-compose自动部署go项目全流程,本地到镜像仓库到服务器,踩坑笔记

声明:个人所学记录,有可以改进的地方希望不吝指教 Dockerfile # 使用golang官方镜像作为构建环境 FROM golang:1.23-alpine AS builder# 设置工作目录 WORKDIR /app# 设置环境变量镜像变量 ENV GO111MODULE=on ENV GOPROXY=https://goproxy.cn,direct# 复制go.mod 和 go.sum文…

docker部署d2l环境

编写dockerfile # 使用NVIDIA提供的CUDA基础镜像,包含CUDA 11.8.0和cuDNN 8,基于Ubuntu 22.04 FROM nvidia/cuda:11.8.0-cudnn8-devel-ubuntu22.04 # 设置维护者信息 MAINTAINER watcherprime <woma@126.com># 设置环境变量,包括时区、非交互式前端和PATH变量 ENV TZ=…

【TCP协议】TCP Keepalive 指南

1、什么是 TCP Keepalive?TCP Keepalive 是一种 TCP 协议内置的探测机制,用于检测长时间未活动的连接是否仍然存活。当启用了 Keepalive 后,TCP 会在连接空闲一定时间后,定期向对端发送探测包,如果未收到对端的响应,则会尝试多次探测,最终关闭连接。 用途: 检测并清理死…

《CPython Internals》阅读笔记:p151-p151

《CPython Internals》学习第 9天,p151-p1510 总结,总计 1 页。 一、技术总结 无。 二、英语总结(生词:1) 1.marshal (1)marshaling Marshalling or marshaling(US spelling) is the process of transforming the memory representation of an object into a data form su…

# vm逆向

vm逆向 虚拟机逆向与实现-CSDN博客 对上面博客的总结。 引 vm逆向题,一般是小型虚拟机程序,可以理解为一种模拟器,有start,dispatcher,opcode等结构。常见使用while-switch/if这类循环+选择结构来实现简单的虚拟机模拟,如下:逆向重点:分析入口,搞清输入和opcode的位置理…

【Gossip 协议】Redis 集群中节点之间的通信方式?

# 分布式系统 # Gossip 协议 在分布式系统中,不同的节点进行数据/信息共享是一个基本的需求。 一种比较简单粗暴的方法就是 集中式发散消息,简单来说就是一个主节点同时共享最新信息给其他所有节点,比较适合中心化系统。这种方法的缺陷也很明显,节点多的时候不光同步消息的…

推荐一款超棒的 Minecraft 启动器:Voxelum/x-minecraft-launcher

X Minecraft Launcher (XMCL) 是一个便于你管理多种整合包、模组、资源包、光影包的现代化启动器。它还支持 Minecraft Forge、 Fabric、Quilt、CurseForge 和 Modrinth它具有以下令人心动的特点:多版本兼容性:支持多个 Minecraft 版本,正式版和愚人节版本。自动化资源下载与…

互联网大中小厂实习面经:滴滴、美团、货拉拉、蔚来、信通院等

本文介绍Momenta、蔚来、中国信息通信研究院、昆仑万维、滴滴、易智瑞等企业各类技术岗位的暑期实习、日常实习面试流程与具体问题~本文介绍Momenta、蔚来、中国信息通信研究院、昆仑万维、滴滴、易智瑞等企业各类技术岗位的暑期实习、日常实习面试流程与具体问题。在前一段时间…

2025年1月买的几个好用的「新物件」

前言 去年底比较忙,换工作+搬家什么的,一堆事情凑在一起,很多24年买的东西还没发开箱 只能后面慢慢补上了,现在先把1月份买的一些东西发一下 红米k80手机 本来我是不想买手机的 不过办了新的套餐,营业厅送了购机补贴,不是很多,所以在补贴范围内选择了「看起来」性价比最…

安卓平板使用can-utils检查can通信功能

安装 termux 打开安卓平板 USB 调试模式,并确保电脑安装了 adb。 下载 termux,通过adb install ./termux.exe安装 termux 到安卓平板。 设置 termux 换源 安装好 termux 后打开,进行换源操作。 termux 中输入termux-change-repo。空格切换选中状态,移动方向键切换选中项目,…