[读书笔记] 《修炼之道:.NET 开发要点精讲》

[读书笔记] 《修炼之道:.NET 开发要点精讲》

目录 显示

修炼之道:.NET 开发要点精讲》

作者:周见智;博图轩


第 1 章 另辟蹊径:解读.NET

1.7 本章思考 > 位置 465

1. 简述. NET 平台 中 CTS、 CLS 以及 CLR 的 含义 与 作用。

A: CTS 指 公共 类型 系统, 是. NET 平台 中 各种 语言 必须 遵守 的 类型 规范; CLS 指 公共 语言 规范, 是. NET 平台 中 各种 语言 必须 遵守 的 语言 规范; CLR 指 公共 语言 运行时, 它是 一个 虚拟 机,. NET 平台 中 所有 的 托管 代码 均需 要在 CLR 中 运行, 可将 其 视为 另外 一个 操作系统。

 

第 2 章 高屋建瓴:梳理编程约定

2.2 方法与线程的关系 > 位置 519

只要 我们 确定 了 两个 方法 只会 运行 在 同一个 线程 中, 那么 这 两个 方法 就不 可能 同时 执行, 跟 方法 所处 的 位置 无关。

只可 能 一前一后 执行, 此时 我们 不需要 考虑 方法 中 访问 的 公共 资源 的 线程 是否 安全。

 

2.7 线程安全 > 位置 595

如果 操作 一个 对象( 比如 调用 它的 方法 或者 给 属性 赋值) 为 非 原子 操作, 即可 能 操作 还没 完成 就 暂停 了, 这个时候 如果 有 另外 一个 线程 开始 运行 同时 也 操作 这个 对象, 访问 了 同样 的 方法( 或 属性), 那么 这时 就可能 会 出现 一个 问题: 前一 个 操作 还未 结束, 后 一个 操作 就 开始 了, 前后 两个 操作 一起 出现 混乱。 当 多个 线程 同时 访问 一个 对象 时, 如果 每次 执行 都得 到 不一样 的 结果, 甚至 出现 异常, 那么 这个 对象 便是 “非 线程 安全”。 造成 一个 对象 非 线程 安全 的 因素 有 很多, 除了 前面 提到 的 由于 非 原子 操作 执行 到 一半 就 中断 以外, 还有 一种 情况 是由 多个 CPU 造成 的, 即 就算 操作 没有 中断, 由于 多个 CPU 可以 真正 实现 多 线程 同时 运行, 所以 就有 可能 出现 “ 对 同一 对象 同时 操作 出现 混乱” 的 情况,如图 2- 4 所示。

%title插图%num

 

2.7 线程安全 > 位置 627

在 Winform 编程 中, 我们 之所以 经常 会 遇见 “不在 创建 控 件 的 线程 中 访问 该 控 件” 的 异常, 原因 就 是对 UI 控 件 的 操作 几乎 都不 是 线程 安全 的( 部分 是)。 一般 UI 控 件 只能 由 UI 线程 操作, 其余 的 所有 操作 均需 要 投递 到 UI 线程 之中 执行, 否则 就会 像 前面 讲过 的 那样, 程序 出现 异常 或不 稳定。

可以 使用 Control. InvokeRequired 属性 去 判断 当前 线程 是否 是 创建 控 件 的 线程( UI 线程), 如果 是, 则 该 属性 返回 false, 可以 直接 操作 UI 控 件, 否则, 返回 true, 不能 直接 操作 UI 控 件。

注: Control 含有 若干个 线程 安全 的 方法 和 属性, 常见 的 主要 有 Control. InvokeRequired 属性、 Control. Invoke 方法、 Control. BeginInvoke 方法( Control. Invoke 的 异步 版本)、 Control. EndInvoke 方法 以及 Control. CreateGraphics 方法。 这些 属性 和 方法 都可以 在 非 UI 线程 中 使用, 并且 跨线 程 访问 这些 方法 和 属性 时不 会 引起 程序 异常。

 

2.8 调用与回调 > 位置 661

.NET 平台 开发 中的 回 调 主要 是 通过 委托 来 实现 的。 委托 是一 种 代理, 专门 负责 调用 方法。

 

2.9 托管资源与非托管资源 > 位置 666

.NET 中 对象 使 用到 的 非 托管 资源 主要 有 I/ O 流、 数据库 连接、 Socket 连接、 窗口 句柄 等 各种 直接 与 操作系统 相关 的 资源。

 

2.13 协议 > 位置 751

%title插图%num

图 2- 11   网络 七 层 协议

 

第 3 章 编程之基础:数据类型

3.1 引用类型与值类型 > 位置 844

通常 值 类型 又 分为 以下 两个 部分。

(1) 简单 值 类型: 包括 类似 int、 bool、 long 等. NET 内置 类型。 它们 本质上 也是 一种 结构 体。

(2) 复合 值 类型: 使用 Struct 关键字 定义 的 结构 体, 比如 System. Drawing. Point 等。 复合 值 类型 可以 由 简单 值 类型 和 引用 类型 组成。

 

3.3 赋值与复制 > 位置 1118

引用 类型 的 赋值、 浅 复制、 深 复制 的 区别, 如图 3- 15 所示。

%title插图%num

 

值 类型 的 赋值、 浅 复制、 深 复制 的 区别, 如图 3- 16 所示。

%title插图%num

 

对象 深 复制 的 过程, 如图 3- 17 所示。

%title插图%num

 

3.3 赋值与复制 > 位置 1155

NET 中 可以 使用 “序列 化 和 反 序列 化” 的 技术 实现 对象 的 深 复制, 只要 一个 类型 及其 所有 成员 的 类型 都 标示 为 “ 可 序列 化”, 那么 就可以 先 序列 化 该类 型 对象 到 字 节流, 然后 再将 字节 流 反序 列 化成 源 对象 的 副本。 这样一来, 源 对象 与 副本 之间 没有 任何 关联, 从而 达到 深 复制 的 效果。

 

第 4 章 物以类聚:对象也有生命

4.1 堆和栈 > 位置 1235

栈 主要 用来 记录 程序 的 运行 过程, 它有 严格 的 存储 和 访问 顺序; 而 堆 主要 存储 程序 运行 期间 产生 的 一些 数据, 几乎没有 顺序 的 概念。

 

4.1 堆和栈 > 位置 1269

堆 跟 栈 的 本质 都是 一段 内存块

 

4.2 堆中对象的出生与死亡 > 位置 1272

栈 中的 对象 由 系统 负责 自动 存入 和 移 除, 正常 情况下, 跟我 们 程序 开发 的 关联 并不 大。

 

4.2 堆中对象的出生与死亡 > 位置 1354

谨慎 使用 对象 的 析构方法。 析 构 方法 由 CLR 调用, 不受 程序控制, 而且 容易 造成 对象 重生。 析 构 方法 除了 用作 管理 非 托管 资源 外, 几乎 不能 用作 其他 用途。

 

4.3 管理非托管资源 > 位置 1381

GC. SuppressFinalize 方法 请求 CLR 不要 再 调用 本 对象 的 析 构 方法, 原因 很 简单, 既然 非 托管 资源 已经 释放 完成, 那么 CLR 就 没 必要 再继续 调用 析 构 方法。

 

注: CLR 调用 对象 的 析 构 方法 是一 个 复杂 的 过程, 需要 消耗 非常 大的 性能, 这也 是 尽量 避免 在 析 构 方法 中 释放 非 托管 资源 的 一个 重要 原因, 最好 是 彻底 地 不 调用 析 构 方法。

 

4.4 正确使用 IDisposable 接口 > 位置 1547

如果 一个 类型 使 用了 非 托管 资源, 或者 它 包含 使 用了 非 托管 资源 的 成员, 那么 开发者 就应 该 应用 “ Dispose 模式”: 正确地 实现( 间接 或 直接) IDisposable 接口, 正确地 重写 Dispose( bool disposing) 虚 方法。

 

4.6 本章思考 > 位置 1587

调用 一个 对象 的 Dispose() 方法 后, 并不 意味着 该 对象 已经 死亡, 只有 GC 将对 象 实例 占用 的 内存 回收 后, 才 可以说 对象 已死。 但是 通常 情况下, 在调 用 对象 的 Dispose() 方法 后, 由于 释 放了 该 对象 的 非 托管 资源, 因此 该 对象 几乎 就 处于 “无用” 状态,“ 等待 死亡” 是它 正确 的 选择。

 

第 5 章 重中之重:委托与事件

5.1 什么是.NET 中的委托 > 位置 1625

像 声明 一个 普通 方法 一样, 提供 方法 名称、 参数、 访问 修饰 符 以及 返回 值, 然后 在前面 加上 Delegate 关键字, 这样 就 定义 了 一个 委托 类型。

委托 类型 定义 完成 后, 怎样 去 实例 化 一个 委托 对象 呢? 其实 很 简单, 跟 实例 化 其他 类型 对象 一样, 我们 可以 通过 new 关键字 创建 委托 对象。

 

5.1 什么是.NET 中的委托 > 位置 1655

使用 委托 调用 方法 时, 我们 可以 直接 使用 “委托对象( 参数 列表);” 这样 的 格式, 它 等效 于 “ 委托对象.Invoke( 参数 列表)”。

给 委托 赋值 的 另外 一种 方式 是: 委托对象 = 方法。

 

怎样 让 一个 委托 同时 调用 两个 或者 两个 以上 的 方法 呢? 答案 是 直接 使用 加法 赋值 运算符( =) 将 多个 方法 附加 到 委托 对象 上。

 

5.1 什么是.NET 中的委托 > 位置 1744

委托 内部 的 “链 表” 结构 跟 单向 链 表 的 实现 原理 却不 相同。 它 并不是 通过 Next 引用 与 后续 委托 建立 关联, 而是 将 所有 委托 存放 在 一个 数组中, 如图 5- 6 所示。

%title插图%num

每一个 委托 类型 都有 一个 公开 的 GetInvocationList() 的 方法, 可以 返回 已 附加 到 委托 对象 上 的 所有 委托, 即 图 5- 6 中 数组 列表 部分。 另外, 我们 平时 不 区分 委托 对象 和 委托 链 表, 提到 委托 对象, 它 很有可能 就 表示 一个 委托 链 表, 这 跟 单向 链 表 只 包含 一个 节点 时 道理 类似。

 

5.1 什么是.NET 中的委托 > 位置 1781

委托 跟 String 类型 一样, 也是 不可改变 的。 换句话说, 一旦 委托 对象 创建 完成 后, 这个 对象 就不能 再被 更改, 那么 我们 前面 讲到 的 将 一个 委托 附加 到 另外 一个 委托 对象 上 形成 一个 委托 链 表 又 该 如何 解释 呢? 其实 这个 跟 String. ToUpper() 过程 类似, 我们 对 委托 进行 附加、 移 除 等 操作 都会 产生 一个 全新 的 委托, 这些 操作 并不 会 改变 原有 委托 对象。

 

5.1 什么是.NET 中的委托 > 位置 1821

框架 有两 种 调用 框架 使用者 编写 的 代码 的 方式, 一种 便是 面向 抽象 编程, 即 框架 中 尽量 不出 现 某个 具体 类型 的 引用, 而是 使用 抽象化 的 基 类 引用 或者 接口 引用 代替。 只要 框架 使用者 编写 的 类型 派生 自 抽象化 的 基 类 或 实现 了 接口, 框架 均可 以 正确地 调用 它们。

框架 调用 框架 使用者 代码 的 另外 一种 方式 就是 使用 委托, 将 委托 作为 参数( 变量) 传递 给 框架, 框架 通过 委托 调用 方法。

 

5.2 事件与委托的关系 > 位置 1860

问题,. NET 中 提出 了 一种 介于 public 和 private 之间 的 另外 一种 访问 级别: 在 定义 委托 成员 的 时候 给出 event 关键字 进行 修饰, 前面 加了 event 关键字 修饰 的 public 委托 成员, 只 能在 类 外部 进行 附加 和 移 除 操作, 而 调用 操作 只能 发生 在 类型 内部。

 

我们 把 类 中 设置 了 event 关键字 的 委托 叫作 “事件”。“ 事件” 本质上 就是 委托 对象。 事件 的 出现, 限制 了 委托 调用 只能 发生 在 一个 类型 的 内部, 如图 5- 12 所示。

%title插图%num

在 图 5- 12 中 由于 server 中的 委托 使用 了 event 关键字 修饰, 因此 委托 只能 在 server 内部 调用, 对 外部 也 只能 进行 附加 和 移 除 方法 操作。 当 符合 某一 条件 时, server 内部 会 调用 委托, 这个 时间 不由 我们( client) 控制, 而是 由 系统( server) 决定。 因此 大部分 时候, 事件 在 程序 中 起 到了 回 调 作用。

 

调用 加了 event 关键字 修饰 的 委托 也称 为 “ 激发事件”。 其中, 调用 方( 图 5- 12 中的 server) 被称为 “ 事件发布者”; 被 调用 方( 图 5- 12 中的 client) 被称为 “ 事件注册者”( 或 “ 事件观察者”、“ 事件订阅者” 等, 本书 中统 一 称之为 “事件 注册 者”); 附加 委托 的 过程 被 称之为 “ 注册事件”( 或 “ 绑定事件”、“ 监听事件”、“ 订阅事件” 等, 本书 中统 一 称之为 “注册 事件”); 移 除 委托 的 过程 被 称之为 “ 注销事件”。 通过 委托 调用 的 方法 被称为 “ 事件处理程序”。

 

5.3 使用事件编程 > 位置 1940

在调 用 委托 链 时, 如果 某一个 委托 对应 的 方法 抛出 了 异常, 那么 剩下 的 其他 委托 将 不会 再 调用。 这个 很容易 理解, 本来 是按 先后 顺序 依次 调用 方法, 如果 其中 某一个 抛出 异常, 剩下 的 肯定 会被 跳过。 为了 解决 这个 问题, 单单 是将 激发 事件 的 代码 放在 try/catch 块 中 是 不够 的, 我们 还 需要 分 步调 用 每个 委托, 将 每 一步 的 调用 代码 均 放在 try/catch 块 中。

 

5.3 使用事件编程 > 位置 1967

除了 事件 本身 的 命名, 事件 所属 委托 类型 的 命名 也 同样 有 标准 格式, 一般以 “ 事件 名  EventHandler” 这种 格式 来给 委托 命名, 因此 前面 提到 的 NewEmailReceived 事件 对应 的 委托 类型 名称 应该 是 “NewEmailReceivedEventHandler”)。 激发 事件 时会 传递 一些 参数, 这些 参数 一般 继承 自 EventArgs 类型( 后者 为. NET 框架 预定 义 类型), 以 “ 事件 名  EventArgs” 来 命名, 比如 前面 提到 的 NewEmailReceived 事件 在 激发 时 传递 的 参数 类型 名称 应该 是 “NewEmailReceivedEventArgs”。

 

5.3 使用事件编程 > 位置 2004

之所以 要把 激发 事件 的 代码 放在 一个 单独 的 虚 方法 中, 是 为了 让 从 该 类型( EmailManager) 派生 出来 的 子类 能够 重写 虚 方法, 从而 改变 激发 事件 的 逻辑。

 

虚 方法 的 命名 方式 一般 为 “ On 事件 名”。 另外, 该 代码 中的 虚 方法 必须 定义 为 “protected”, 因为 派生 类 中 很可能 要 调用 基 类 的 虚 方法。

 

5.3 使用事件编程 > 位置 2039

%title插图%num

图 5- 13   属性 和 事件 的 作用

 

5.4 弱委托 > 位置 2077

在 事件 编程 中, 委托 的 Target 成员, 就是 对 事件 注册 者 的 强 引用, 如果 事件 注册 者 没有 注销 事件, 强 引用 Target 便会 一直 存在, 堆 中的 事件 注册 者 内存 就 一直 不 会被 CLR 回收, 这对 开发 人员 来讲, 几乎 是 很难 发觉 的。

 

像 “A a= new A();” 中的 a 被称为 “ 显式强引用( Explicit Strong Reference)”, 类似 委托 中 包含 的 不明显 的 强 引用, 我们 称之为 “ 隐式强引用( Implicit Strong Reference)”。

弱引用 与 对象 实例 之间 属于 一种 “弱 关联” 关系, 跟 强 引用 与 对象 实例 的 关系 不一样, 就算 程序 中有 弱 引用 指向 堆 中 对象 实例, CLR 还是 会把 该 对象 实例 当作 回收 目标。 程序 中 使用 弱 引用 访问 对象 实例 之前 必须 先 检查 CLR 有没有 回收 该 对象 内存。 换句话说, 当 堆 中 一个 对象 实例 只有 弱 引用 指向 它 时, CLR 可以 回收 它的 内存。 使用 弱 引用, 堆 中 对象 能否 被 访问, 同时 掌握 在 程序 和 CLR 手中。

创建 一个 弱 引用 很 简单, 使用 WeakReference 类型, 给 它的 构造 方法 传递 一个 强 引用 作为 参数 即可。

 

5.4 弱委托 > 位置 2105

在 编程 过程中, 由于 很难 管理 好强 引用, 从而 造成 不必 要的 内存 开销。 尤其 前面 讲到 的 “隐式 强 引用”, 在 使用 过程中 不易 发觉 它们 的 存在。 弱 引用 特别 适合 用于 那些 对 程序 依赖 程度 不高 的 对象, 即那 些 对象 生命 期 主要 不是 由 程序控制 的 对象。 比如 事件 编程 中, 事件 发布者 对 事件 注册 者 的 存在 与否 不是 很 关心, 如果 注册 者 在, 那就 激发 事件 并 通知 注册 者; 如果 注册 者 已经 被 CLR 回收 内存, 那么 就不 通知 它, 这 完全 不会 影响 程序 的 运行。

 

5.4 弱委托 > 位置 2109

前面 讲到 过, 委托 包含 两个 部分: 一个 Object 类型 的 Target 成员, 代表 被 调用 方法 的 所有者, 如果 方法 为 静态 方法, 则 Target 为 null; 另一个 是 MethodInfo 类型 的 Method 成员, 代表 被 调用 方法。 由于 Target 成员 是 一个 强 引用, 所以 只要 委托 存在, 那么 方法 的 所有者 就会 一直 在 堆 中 存在 而 不 能被 CLR 回收。 如果 我们将 委托 中的 Target 强 引用 换成 弱 引 用的 话, 那么 不管 委托 存在 与否, 都不 会 影响 方法 的 所有者 在 堆 中 内存 的 回收。 这样一来, 我们 在使 用 委托 调用 方法 之前, 需要 先 判断 方法 的 所有者 是否 已经 被 CLR 回收。 我们 称 将 Target 成员 换成 弱 引用 之后 的 委托 为 “ 弱委托”, 弱 委托 定义 代码 如下:

//Code 5- 26 
class WeakDelegate 
{ 
    WeakReference _weakRef; //NO. 1 
    MethodInfo _method; //NO. 2 
    
    public WeakDelegate(Delegate d) 
    { 
        _weakRef = new WeakReference(d.Target); 
        _methodInfo = d.Method; 
    } 
    
    public object Invoke( param object[] args) 
    {
        object obj = _weakRef.Target; 
        if(_weakRef.IsAlive) //NO. 3 
        { 
            return _method.Invoke(obj, args); //NO. 4 
        } 
        else 
        { 
            return null; 
        } 
    } 
}

 

弱 委托 将 委托 与 被 调用 方法 的 所有者 之间 的 关 系由 “强 关联” 转换 成了 “ 弱 关联”, 方法 的 所有者 在 堆 中的 生命 期 不再 受 委托 的 控制, 如图 5- 16 所示, 为 弱 委托 的 结构。

%title插图%num

本 小节 示例 代码 中的 WeakDelegate 类型 并没有 提供 类似 Delegate. Combine 以及 Delegate. Remove 这样 操作 委托 链 表 的 方法, 当然 也 没有 弱 委托 链 表 的 功能。 这些 功能 可以 仿照 单向 链 表 的 结构 去 实现, 把 每个 弱 委托 都 当作 链 表中 的 一个 节点。 其 方法 可 参照 5. 1. 2 小节 中 讲到 的 单向 链 表。

 

5.5 本章回顾 > 位置 2151

委托 的 3 个 作用: 第一, 它 允许 把 方法 作为 参数, 传递 给 其他 的 模块; 第二, 它 允许 我们 同时 调用 多个 具有 相同 签名 的 方法; 第三, 它 允许 我们 异步 调用 任何 方法。 这 3 个 作用 奠定 了 委托 在. NET 编程 中的 绝对 重要 地位。

 

第 6 章 线程的升级:异步编程模型

6.1 异步编程的必要性 > 位置 2171

通常 情况下, 调用 一个 方法 都 符合 这样 一个 规律: 调用 线程 开始 调用 方法 A 后, 在 A 返回 之前, 调用 线程 得不到 程序 执行 的 控制 权。 也就是说, 方法 A 后面 的 代码 是 不可能 执行 的, 直到 A 返回 为止, 这种 调用 方式 被 称之为 “ 同步调用”; 相反, 如果 调用 在 返回 之前, 调用 线程 依旧 保留 控制 权, 能够 继续 执行 后面 的 代码, 那么 这种 调用 方式 被称为 “ 异步调用”。

 

同步 调用 也 被 一些 学者 称为 “ 阻塞调用”; 一些 相对 的 异步 调用 则 被称为 “ 非阻塞调用”。

 

6.2 委托的异步调用 > 位置 2202

理论上 讲, 任何 一个 方法, 通过 委托 包装 后, 都可以 实现 异步 调用。

 

.NET 编译器 定义 的 每个 委托 类型 都 自动 生成 了 两个 方法: BeginInvoke 和 EndInvoke。 这 两个 方法 专门 用来 负责 异步 调用 委托。

BeginInvoke 返回 一个 IAsyncResult 接口 类型, 它可 以 唯一 区分 一个 异步 调用 过程。 BeginInvoke 一 执行 就能 马上 返回, 不会 阻塞 调用 线程。 EndInvoke 表示 结束 对 委托 的 异步 调用, 但这 并不 意味着 它可 以 中断 异步 调用 过程, 如果 异步 调用 还未 结束, EndInvoke 则 只能 等待, 直到 异步 调用 过程 结束。 另外, 如果 委托 带有 返回 值, 我们 必须 通过 EndInvoke 获得 这个 返回 结果。

 

6.2 委托的异步调用 > 位置 2233

委托 异步 调用 开始 后, 系统 会在 线程 池 中 找到 一个 空闲 的 线程 去 执行 委托。

 

6.2 委托的异步调用 > 位置 2296

异步 调用 委托 时, 由于 方法 实际 运行 在 其他 线程 中( 线程 池 中的 某一 线程, 非 当前 调用 线程), 因此 当前 线程 捕获 不了 异常, 那么 我们 怎样 才能 知道 异步 调用 过程中 到底 是否 会有 异常 呢? 答案 就在 EndInvoke 方法 上, 如果 异步 调用 过程 有 异常, 那么 该 异常 就会 在 我们 在 调用 EndInvoke 方法 时 抛出。 所以 我们 在 调用 EndInvoke 方法 时, 一定 要把 它 放在 try/catch 块 中。

 

6.3 非委托的异步调用 > 位置 2358

.NET 中 提供 异步 方法 的 类型 有 Stream( 或 其 派生 类)、 Socket( 或 其 派生 类) 以及 访问 数据库 的 SqlConnection 类型 等。 它们 的 使用 方式 跟 委托 的 BeginInvoke 和 EndInvoke 方法 类似, 只是 命名 有所 差别, 基本上 都是 “Begin 操作” 和 “ End 操作” 这种 格式。 比如 FileStream. BeginRead 表示 开始 一个 异步 读 操作, 而 FileStream. EndRead 则 表示 结束 异步 读 操作。

 

6.6 本章思考 > 位置 2446

异步 编程 与 多 线程 编程 的 效果 类似, 都是 为了 能够 并行 执行 代码, 达到 同时 处理 任务 的 目的。 异步编程 时, 系统 自己 通过 线程池 来 分配 线程, 不需要 人工干预, 异步 编程 逻辑 复杂 不易 理解, 而 多 线程 编程 时, 完全 需要 人为 去 控制, 相对 较 灵活。

 

第 7 章 可复用代码:组件的来龙去脉

7.1 .NET 中的组件 > 位置 2457

在. NET 编程 中, 我们 把 实现( 直接 或者 间接) System.ComponentModel.IComponent 接口 的 类型 称为 “组件”,

 

7.1 .NET 中的组件 > 位置 2472

组件 和 控 件 不是 相等 的, 组件包含控件, 控 件 只是 组件 中的 一个 分类。

 

7.1 .NET 中的组件 > 位置 2477

%title插图%num

图 7- 2   Windows Forms 中 控 件 之间 的 关系

 

所 有的 控 件 均 派生 自 Control 类, Control 类 又 属于 组件, 因此 所有 控 件 均 具备 组件 的 特性。

不管 组件 还是 控 件, 它们 都是 可以 重复 使用 的 代码 集合, 都 实现 了 IDisposable 接口, 都 需要 遵循 第 4 章 中 讲到 的 Dispose 模式。如果 一个 类型 使 用了 非 托管 资源, 它 实现 IDisposable 接口 就可以 了, 那 为什么 还要 在. NET 编程 中 又 提出 组件 的 概念 呢?

这样做 可以 说完 全是 为了 实现 程序 的 “ 可视化开发”, 也就是 我们 常说 的 “所见 即 所得”。 在 类似 Visual Studio 这样 的 开发 环境 中, 一切 “ 组件” 均 可被 可 视 化 设计, 换句话说, 只要 我们 定义 的 类型 实现 了 IComponent 接口, 那么 在开 发 阶段, 该 类型 就可以 出现 在窗 体 设计 器 中, 我们 就可以 使用 窗体 设计 器 编辑 它的 属性、 给 它 注册 事件。 它 还能 被 窗体 设计 器 中 别的 组件 识别。

 

7.2 容器 – 组件 – 服务模型 > 位置 2499

在. NET 编程 中, 把 所有 实现( 直接 或 间接) System. ComponentModel.IContainer 接口 的 类型 都 称之为 逻辑 容器( 以下 简称 “容器”)。

容器 是 为 组件 服务 的。

.NET 框架 中有 一个 IContainer 接口 的 默认 实现: System. ComponentModel.Container 类型, 该类 型 默认 实现 了 IContainer 接口 中的 方法 以及 属性。

 

7.2 容器 – 组件 – 服务模型 > 位置 2514

传统 容器 仅仅 是在 空间 上 简单 地 将 数据 组织 在一起, 并不 能为 数据 之间 的 交互 提供 支持。 而 本章 讨论 的 逻辑容器, 在 某种 意义上 讲, 更为 高级。 它 能为 组件( 逻辑 元素) 之间 的 通信 提供 支持, 组件 与 组件 之间 不再 是 独立 存在。 此外, 它 还能 直接 给 组件 提供 某些 服务。物理 容器 和 逻辑 容器 分别 与 元素 之间 的 关系, 如图 7- 4 所示。

%title插图%num

物理 容器 中的 元素 之间 不能 相互 通信, 物理 容器 也不 可能 为 其内 部 元素 提供 服务; 逻辑 容器 中的 组件 之间 可以 通过 逻辑 容器 作为 桥梁, 进行 数据 交换; 同时, 逻辑 容器 还能 给 各个 组件 提供 服务。 所谓 服务, 就是 指 逻辑 容器 能够 给 组件 提供 一些 访问 支持。 比如 某个 组件 需要 知道 它的 所属 容器 中共 包含 有 多少 个 组件, 那么 它 就 可以向 容器 发出 请求。 容器 收到 请求 后 会为 它 返回 一个 获取 组件 总数 的 接口。

 

在 本章 7. 1. 1 小节 中 我们 提 到过 IComponent 接口 中有 一个 ISite 类型 的 属性。 当时 说 它是 起到 一个 “定位” 的 作用。 现在 看来, 组件 与 容器 之间 的 纽带 就 是它, 组件 通过 该 属性 与 它 所属 容器 取得 了 联系。

 

7.2 容器 – 组件 – 服务模型 > 位置 2574

Component、 Site 以及 Container 3 个 类型 均 包含 有 获取 服务 的 方法 GetService。 现在 我们 可以 整理 一下 组件 向 容器 请求 服务 的 流程, 如图 7- 6 所示。
%title插图%num

 

注: 容器 将 组件 添加 进来 时( 执行 Container. Add), 会 初始化 该 组件 的 Site 属性, 让 该 组件 与 容器 产生 关联, 只有 当 这一 过程 发生 之后, 组件 才能 获取 容器 的 服务。

 

7.2 容器 – 组件 – 服务模型 > 位置 2601

在 我们 向 窗体 设计 器 中 拖动控件 时, 是 会 执行 类似 “new Button();” 这样 的 代码, 在 内存 中 实例化 一个 组件 实例。

 

7.2 容器 – 组件 – 服务模型 > 位置 2655

%title插图%num

图 7- 10   窗体 设计 器 中的 组件 与 生成 的 源 代码

在 图 7- 10 中, 图中 左边 显示 我们 拖放 到 设计 器 中的 一个 Button 控 件。 在 这个 过程中, 窗体 设计 器 除了 会 实例 化 一个 Button 控 件( 图中 左边 Form2 中), 还会 为我 们 生成 右边 的 代码。

 

7.3 设计时与运行时 > 位置 2672

任何 组件 都有 两种 状态: 设计时 和 运行时。

判断 组件 的 当前状态 有 以下 两种 方法。

(1) 判断 组件 的 DesignMode 属性。 每个 组件 都有 一个 Bool 类型 的 DesignMode 属性, 正如 它的 字面 意思, 如果 该 属性 为 true, 那么 代表 组件 当前 处于 设计 时 状态; 否则 该 组件 处于 运行时 状态。

(2) 随便 请求 一个 服务, 看 返回 来的 服务 接口 是否 为 null。 前面 提 到过, 当 一个 组件 不属于 任何 一个 容器 时, 那么 它 通过 GetService 方法 请求 的 服务 肯定 返回 为 null。

 

注:(1)(2) 方法 均不 适合 嵌套组件, 因为 窗体 设计 器 只会 将 最外 层 组件 的 DesignMode 属性 值 设置 为 true。

有 一种 可以 解决 嵌套 组件 中 无法 判断 其 子 组件 状态 的 方法, 那就 是 通过 Process 类 来 检查 当前 进程 的 名称。 看 它是 否 包含 “devenv” 这个 字符串。 如果 有, 那么 说明 组件 当前 处于 Visual Studio 开发 环境 中( 即 组件 处于 设计 时), if( Process. GetCurrentProcess (). ProcessName. Contains(”devenv”)) 为 假, 说明 组件 处于 运行时。 这种 方法 也有 一个 弊端, 很 明显, 如果 我们 使用 的 不是 Visual Studio 开发 环境( 即 进程 名 不 包含 devenv), 或者 我们自己 的 程序 进程 名称 本身 就 已经 包含 了 devenv, 那么 该 怎么办 呢?

 

在开 发 一些 需要 授权 的 组件 时, 就可以 用到 组件 的 两种 状态。 这些 需要 授权 的 组件 收费 对象 一般 是 开发者。 因此, 在 开发者 使用 这些 组件 开发 系统 的 时候( 处于 开发 阶段), 就应 该有 授权 入口, 而 当 程序 运行 之后, 就不 应该 出现 授权 的 界面。

 

7.4 控件 > 位置 2776

无论是 复合 控 件、 扩展 控 件 还是 自定义 控 件, 我们 均可 以 重写 控 件 的 窗口过程: WndProc 虚 方法, 从 根源 上 接触 到 Windows 消息, 这个 做法 并不是 自定义 控 件 的 专利。

 

第 8 章 经典重视:桌面 GUI 框架揭秘

8.2 Win32 应用程序的结构 > 位置 2841

程序 是 无法 直接 识别 用户 键盘 或者 鼠标 等 设备 的 输入 信息, 这些 输入 必须 先由 操作系统 转换 成 固定 格式 数据 之后, 才能 被 程序 使用。

在 Windows 编程 中, 我们 把 由 操作系统 转换 之后 的 固定 格式 数据 称为 Windows 消息。 Windows 消息 是一 种 预 定义 的 数据 结构( 比如 C 中的 Struct), 它 包含 有 消息 类型、 消息 接收者 以及 消息 参数 等 信息。 我们 还把 程序 中 获取 Windows 消息 的 结构 称之为 Windows 消息循环。 Windows 消息 循环 在 代码 中就 是一 个 循环 结构( 比如 while 循环), 它 不停 地 从 操作系统 中 获取 Windows 消息, 然后 交给 程序 去 处理。

 

8.2 Win32 应用程序的结构 > 位置 2895

在 Windows 中, 其实 将 消息 分成 了 两类, 一类 需要 存入 消息 队列, 然后 由 消息 循环 取出 来之 后才 能被 窗口 过程 处理, 这类 消息 被 称之为 “ 队列消息”( Queued Message)。 这类 消息 主要 包括 用户 的 鼠标 键盘 输入 消息、 绘制 消息 WM_ PAINT、 退出 消息 WM_ QUIT 以及 时间 消息 WM_ TIMER。 另 一类 是 不需要 存入 消息 队列, 也不 经过 消息 循环, 它们 直接 传递 给 窗口 过程, 由 窗口 过程 直接 处理, 这类 消息 被 称之为 “ 非队列消息”( Nonqueued Message)。 当 操作系统 想要 告诉 窗口 发生了 某 件事 时, 它 会 给 窗口 发送 一个 非 队列 消息, 比如 当 我们 使用 SetWindowPos API 移动 窗口 后, 系统 自动 会 发送 一个 WM_ WINDOWPOSCHANGED 消息 给 该 窗口 的 窗口 过程, 告诉 它 位置 发生 变化 了。

 

8.4 Windows Forms 框架 > 位置 3148

在 Windows Forms 框架 中, 以 Control 为 基 类, 其他 所有 与 窗体 显示 有关 的 控 件 几乎 都 派生 自 它; Control 类 中的 WndProc 虚 方法 就是 我们 在 Win32 开发 模式 中 所 熟悉 的 窗口 过程。 另外, 前面 也 讲到 过, 窗体 和 控 件 本质上 是一 个 东西, 只是 它们 有着 不同 的 属性, 所以 我们 可以 看到, 窗体 类 Form 间接 派生 自 Control 类。

 

Winform 程序 中 包含 3 个 部分: 消息 队列、 UI 线程 以及 控 件。

 

8.5 Winform 程序的结构 > 位置 3180

在 每个 Winform 程序 的 Program. cs 文件 中, 都有 一个 Main 方法, 该 Main 方法 就是 程序 的 入口 方法。 每个 程序 启动 时 都会 以 Main 方法 为 入口, 创建 一个 线程, 这个 线程 就是 UI 线程。 可能 你会 问 UI 线程 怎么 没有 消息 循环 呢? 那是 因为 Main 方法 中 总是 会 出现 一个 类似 Application. Run 的 方法, 而 消息 循环 就 隐 藏在 了 该 方法 内部( 具体 参见 下一 小节 内容)。 一个 程序 理论上 可以 有 多个 UI 线程, 且 每个 线程 都有 自己的 消息 队列( 由 操作系统 维护)、 消息 循环、 窗体 等 元素, 如图 8- 13 所示。

%title插图%num

 

由于 UI 线程 之间 的 数据 交换 比较 复杂, 因此 在 实际 开发 中, 在 没有 特殊 需求 的 情况下, 一个 程序 一般 只 包含 有一个 UI 线程。

 

8.5 Winform 程序的结构 > 位置 3265

对 UI 线程 的 认识:

① 一个 程序 可以 包含 有 多个 UI 线程, 我们 完全可以 通过 System. Threading. Thread 类 新创 建 一个 普通 线程, 然后 在 该 线程 中 调用 Application. Run 方法 来 开启 消息 循环;

② 一个 UI 线程 结束( 该 线程 中的 消息 循环 个数 为 “0”) 后, 将会 激发 Application. ThreadExit 事件, 告知 有 UI 线程 结束, 只有 当 所有 UI 线程 都 结束( 程序 中 消息 循环 总数 为 “ 0”), 才会 激发 Application. Exit 事件, 告知 应用 程序 退出。

最后 我们 再来 看一下 Windows Forms 中 消息 循环 的 结构图, 如图 8- 14 所示。

%title插图%num

 

8.5 Winform 程序的结构 > 位置 3319

Windows Forms 框架 将 窗体 和 窗口 过程 封 装在 了 一起, Control( 或 其 派生 类, 下同) 类 中的 WndProc 虚 方法 就是 控 件 的 窗口 过程。 之所以 将 窗口 过程 声明 为 虚 方法, 这是 因为 Windows Forms 框架 是 面向 对象 的, 它 充分 地利 用了 面向 对象 编程 中的 “多 态” 特性。 因此, 所有 Control 类 的 派生 类 均可 以 重写 它的 窗口 过程, 从而 从 源头 上 拦截 到 Windows 消息, 处理 自己 想要 处理 的 Windows 消息。

 

窗口 过程 做了 一个 非常 重要的 事: 将 Window 消息 转换 成了. NET 中的 事件。

 

第 9 章 沟通无碍:网络编程

9.1 两种 Socket 通信方式 > 位置 3518

TCP)数据 是按 顺序 走在 建立 的 一条 隧道 中, 那么 数据 就应 该 遵循 “先走 先 到达” 的 规则, 并且 隧道 中的 数据 以 “ 流” 的 形式 传输。 发送 方 发送 的 前后 两次 数据 之间 没有 边界, 需要 接收 方自 己 根据 事先 规定 好的 “ 协议” 去 判断 数据 边界。

 

9.1 两种 Socket 通信方式 > 位置 3555

UDP 通信 中, 数据 是以 “数 据报” 的 形式 传输, 以 一个 整体 发送、 以 一个 整体 接收, 因此 UDP 存在 数据 边界。 但是 UDP 接 收到 的 数据 是 无序 的, 先 发送 的 可能 后接 收, 后 发送 的 可能 先 接收, 甚至 有的 接收 不到。

 

9.1 两种 Socket 通信方式 > 位置 3605

在. NET 中 有关 Socket 通信 编程 的 类型 主要 有 5 种, 见表 9- 1。

%title插图%num

 

TcpListener 和 TcpClient 的 关系 如图 9- 9 所示

%title插图%num

图 9- 9 中, TcpListener 侦听 来自 客户 端 的 “连接” 请求, 返回 一个 代理 TcpClient, 该 代理 与 客户 端 的 TcpClient 进行 数据 交换。

UdpClient 在 UDP 通信 中 所处 的 角色 如图 9- 10 所示:

%title插图%num

 

NetworkStream 类型 是 System. IO. Stream 类 的 一个 派生 类。NetworkStream 只能 用于 TCP 通信 中。

Socket 类型 是一 个 相对 较 基础 的 通信 类型。 它 既能 实现 TCP 通信 也能 实现 UDP 通信, 可以 认为 TcpListener、 TcpClient 以及 UdpClient 进一步 封装 了 Socket 类型。

 

9.3 UDP 通信的实现 > 位置 3840

之所以 将 TCP 通信 中 应用 层 协议 的 数据 结构设计 成 字节流 的 形式, 是因为 TCP 通信 中 数据 是以 流的 形式 传输, 以 字节 流的 形式 格式化 数据 后, 更 方便 程序 判断 数据 边界。

而在 UDP 通信 中, 数据 以 数据 报的 形式 传输, 每次 接 收到 的 数据 是 完整 的, 对于 当前 局域网 即时 通信 实例 来讲, 协议 使用 文本 的 形式 更 方便 程序 处理 数据, 当然, 我们 完全 也可以 将 UDP 通信 中 应用 层 协议 的 数据 结构设计 成 字节 流的 形式。

 

9.4 异步编程在网络编程中的应用 > 位置 3991

使用 Socket. BeginSendTo() 方法 开始 一个 异步 发送 过程, 并为 该 方法 提供 一个 AsyncCallback 的 回 调 参数。 该 方法 的 调用 不会 阻塞 调用 线程。 我们 在 回 调 方法 OnSend 中 可使用 Socket. EndSendTo() 方法, 结束 异步 发送 过程, 该 方法 返回 实际 发送 的 数据 长度。

 

9.4 异步编程在网络编程中的应用 > 位置 4011

异步 编程 也能 实现 循环 接收 数据, 但却 看 不到 显 式 地 创建 的 线程, 也 看不 到 类似 while 这样 的 循环 语句。

 

9.6 本章思考 > 位置 4062

所有 的 通信协议 本质上 都是 一种 数据结构。 通信 双方 都 必须 按照 这种 数据 结构 规定 的 形式 去 发送 或 接收( 解析) 数据。

基于 TCP 协议 的 通信 在 进行 数据 交互 之前 需要 先 建立 连接, 类似 打电话。 这种 通信 方式 保证 了 数据 传输 的 正确性、 可靠性。 基于 UDP 协议 的 通信 在 进行 数据 传输 之前 不需要 建立 连接, 类似 发 短信。 这种 通信 方式 不能 保证 数据 传输 的 正确性。

 

第 10 章 动力之源:代码中的 “泵”

10.2 常见的 “泵” 结构 > 位置 4150

桌面 程序 的 UI 线程 中 包含 一个 消息 循环( 确切 地说, 应该 是 While 循环)。 该 循环 不断 地 从 消息 队列 中 获取 Windows 消息, 最终 通过 调用 对应 的 窗口 过程, 将 Windows 消息 传递 给 窗口 过程 进行 处理。

 

10.2 常见的 “泵” 结构 > 位置 4179

浏览器 每次 发送 http 请求 时, 都 必须 与 Web 服务器 建立 连接。 Web 服务器 端 请求 处理 结束 后, 连接 立刻 关闭。 浏览器 下一 次 发送 http 请求 时, 必须 再一次 重新 与 服务器 建立 连接。 由此可见, 我们 所说 的 HTTP 协议 是 面向 无 连接 的, 具体 指 Web 服务器 一次 连接 只 处理 一个 请求, 请求 处理 完毕 后, 连接 关闭, 浏览器 在前 一次 请求 结束 到下 一次 请求 开始 之前 这段 时间, 它是 处于 “断开” 状态 的, 因此 称 HTTP 协议 是 “ 无连接” 协议。

Web 服务器 除了 跟 浏览器 之间 不会 保持 持久 性的 连接 之外, 它 也不 会 保存 浏览器 的 状态。 也就是说, 同一 浏览器 先后 两次 请求 同一个 Web 服务器, 后者 不会 保留 第一次 请求 处理 的 结果 到 第二次 请求 阶段; 如果 第二次 请求 需要 使用 第一次 请求 处理 的 结果, 那么 浏览器 必须 自己 将 第一次 的 处理 结果 回 传到 服务器 端。

 

第 11 章 规绳矩墨:模式与原则

11.1 软件的设计模式 > 位置 4308

程序 的 运行 意味着 模块 与 模块 之间、 对象 与 对象 之间 不停 地 有数 据 交换。 观察者模式 要 强调 的 是, 当 一个 目标 本身 的 状态 发生 改变( 或者 满足 某一 条件) 时, 它 会 主动 发出通知, 通知 对 该 变化 感兴趣 的 其他 对象。 将 通知者 称为 Subject( 主体), 将被 通知者 称为 Observer( 观察者),

 

11.1 软件的设计模式 > 位置 4358

“观察者 模式” 是 所有 框架 使用 得 最 频繁 的 设计 模式 之一。 原因 很 简单,“ 观察者 模式” 分隔 开了 框架 代码 和 框架 使用者 编写 的 代码。 它是 “ 好莱坞原则”( Don\’ t call us, we will call you) 的 具体 实现 手段, 而 “好莱坞 原则” 是 所有 框架 都要 严格遵守 的。

 

11.1 软件的设计模式 > 位置 4361

在 Windows Forms 框架 中, 可以说 “观察者 模式” 无处不在。 Windows Forms 框架 中的 “ 观察者 模式” 不是 通过 “ 接口 – 具体” 这种 方式 去 实现 的, 而是 更多 地 通过 使用. NET 中的 “ 委托 – 事件” 去 实现。 这 在 第 8 章 讲 Winform 程序 结构 时 已经 有所 说明, 比如 控 件 处理 Windows 消息 时, 最终 是以 “事件” 的 形式 通知 事件 注册 者, 那么 这里 的 事件注册者 就是 观察者 模式 中的 “ 观察者”, 控件 就是 观察者 模式 中的 “ 主体”。

可以 认为, 事件 的 发布者 等于 观察者 模式 中的 “主体”( Subject), 而 事件 的 注册 者 等于 观察者 模式 中的 “ 观察者”,

 

11.1 软件的设计模式 > 位置 4397

根据 各种 设计 模式 的 作用, 将 常见 的 23 种 设计模式 分为 3 大类, 见表 11- 1。 

%title插图%num

 

11.5 本章思考 > 位置 4605

五大原则 及 英文 全称 分别 如下。

① 单一 职责 原则( Single Responsibility Principle)。

② 开闭 原则( Open Closed Principle)。

③ 里 氏 替换 原则( Liskov Substitution Principle)。

④ 接口 隔离 原则( Interface Segregation Principle)。

⑤ 依赖 倒置 原则( Dependency Inversion Principle)。

 

第 12 章 难免的尴尬:代码依赖

12.1 从面向对象开始 > 位置 4738

类继承 强调 “我是( Is- A)” 的 关系, 派生 类 “ 是” 基 类( 注意 这里 的 “ 是” 代表 派生 类 具备 基 类 的 特性), 而 接口继承 强调 “我 能做( Can- Do)” 的 关系, 实现 了 接口 的 类型 具有 接口 中 规定 的 行为 能力( 因此 接口 在 命名 时 均以 “ able” 作为 后缀)。

 

12.1 从面向对象开始 > 位置 4743

在使 用 继承 时, 应 遵循 以下 准则

(1) 严格遵守 “里 氏 替换 原则”, 即 基 类 出现 的 地方, 派生 类 一定 可以 出现。 因此, 不要 盲目 地 去使 用 继承, 如果 两个 类 没有 衍生 的 关系, 就不 应该 有 继承 关系。

(2) 由于 派生 类 会 继承 基 类 的 全部 内容, 所以 要 严格控制 好 类型 的 继承 层次, 不然 派生 类 的 体积 会 越来越大。 另外, 继承 是 增加 耦合 的 最重要 因素, 基 类 的 修改 必然会 影响 到 派生 类。

(3) 继承 强调 类型 之 间的 通 性, 而非 特性。 因此 一般 将 类型 都 具有 的 部分 提取 出来, 形成 一个 基 类( 抽象 类) 或者 接口。

 

12.2 不可避免的代码依赖 > 位置 4815

为了 衡量 对象 之间 依赖 程度 的 高低, 引进 了 “ 耦合” 这一 概念。 耦合 度 越高, 说明 对象 之间 的 依赖 程度 越高。 为了 衡量 对象 独立 性的 高低, 引进 了 “ 内聚” 这一 概念。 内聚性 越高, 说明 对象 与外 界 交互 越少, 独立性 越 强。 很 明显, 耦合 与 内 聚 是 两个 相互 对立 又 密切相关 的 概念。

 

12.3 降低代码依赖 > 位置 4936

除了 上面 说到 的 将相 同 部分 提取 出来 放到 一个 接口 中, 有时候 还需 要将 相同 部分 提取 出来, 生成 一个 抽象化 的 基 类, 如 抽象类。 接口 强调 相同 的 行为, 而 抽象 类 一般 强调 相同 的 属性, 并且 要使 用在 有 族群 层次 的 类型 设计 中。

 

12.3 降低代码依赖 > 位置 4987

通过 属性 产生 的 依赖 关系(属性注入) 比较 灵活, 它的 有效期 一般 介于 “构造 注入” 和 “ 方法 注入” 之间。

在 很多 场合, 3 种 依赖注入 的 方式 可以 组合 使用, 即 可以 先 通过 “构造 注入” 让 依赖 者 与 被 依赖 者 产生 依赖 关系, 后期 再 使用 “ 属性 注入” 的 方式 更改 它们 之间 的 依赖 关系。“ 需要 注意 的 是, 依赖 注入” 是以 “ 依赖 倒置”” 为 前提 的。

 

12.4 框架的 “代码依赖” > 位置 4998

注:“ 控制转换、 依赖倒置 以及 依赖注入 是 3 个 不同 性质 的 概念。“ 控制转换” 强调 程序控制 权 的 转移, 注重 软件 运行 流程;“ 依赖倒置” 是一 种 降低 代码 依赖 程度 的 理论 指导思想, 它 注重 软件 结构;“ 依赖注入” 是对 象之 间 产生 依赖 关系 的 一种 具体 实现 方式, 它 注重 编程 实现。

“控制 转换” 又称 “ 好莱坞 原则”, 它 建议 框架 与 开发者 编写 代码 之间 的 关系 是 Don\’ t call us, we will call you, 即 整个 程序 的 主动权 在 框架 手中。

 

12.6 本章思考 > 位置 5022

“依赖 倒置 原则” 中的 “ 倒置” 二字 作 何 解释?

A: 正常 逻辑思维 中, 高层 模块 依赖 底层 模块 是 天经地义、 理所当然 的, 而 “依赖 倒置 原则” 建议 我们 所有 的 高层 模块 不应该 直接 依赖于 底层 模块, 而 都 应该 依赖于 一个 抽象。 这里 的 “ 倒置” 二字 并不是 “ 反过来” 的 意思( 即 底层 模块 反过来 依赖于 高层 模块), 它 只是 说明 正常 逻辑思维 中的 依赖 顺序 发生了 变化, 把 所有 违背 了 正常 思维 的 东西 都 称之为 “ 倒置”。

 

PDF 下载地址:https://mianbaoduo.com/o/bread/aZmcmJw=

 

原创文章,转载请注明: 转载自 独立观察员(dlgcy.com)

本文链接地址: [[读书笔记] 《修炼之道:.NET 开发要点精讲》](https://dlgcy.com/dotnet-program-key-point/)

关注微信公众号 独立观察员博客(DLGCY_BLOG) 第一时间获取最新文章

%title插图%num

发表评论