面向对象
C# 是纯粹的面向对象语言,AutoCAD 和 Revit API 本身就是巨大的类层次结构。理解 OOP 是流畅使用这些 API 的前提。
类与对象
public class Circle
{
// 字段(私有,小写开头)
private double _radius;
// 属性(公开,大写开头)
public double Radius
{
get => _radius;
set
{
if (value <= 0)
throw new ArgumentException("半径必须为正数");
_radius = value;
}
}
// 自动属性(简洁写法)
public string Name { get; set; } = "未命名";
// 计算属性(只读)
public double Area => Math.PI * _radius * _radius;
// 构造函数
public Circle(double radius)
{
Radius = radius;
}
// 方法
public void Scale(double factor)
{
Radius *= factor;
}
}💡 在 AutoCAD 开发中,你经常需要派生
ICommand接口或继承DrawJig等基类。属性比公开字段更受 API 青睐。
继承
// 基类
public class Entity
{
public string Id { get; set; }
public string Layer { get; set; }
public virtual void Draw() // virtual = 可被子类重写
{
Console.WriteLine("绘制实体");
}
}
// 派生类
public class Line : Entity // C# 是单继承
{
public double Length { get; set; }
public override void Draw() // override = 重写基类 virtual 方法
{
base.Draw(); // 调用基类实现
Console.WriteLine($"绘制直线,长度:{Length}");
}
}
public class Circle : Entity
{
public double Radius { get; set; }
public override void Draw()
{
Console.WriteLine($"绘制圆,半径:{Radius}");
}
}🔬 AutoCAD API 中,
Curve继承自Entity,Line、Arc、Circle、Polyline又继承自Curve。这种层次让多态操作变得自然——你可以用Curve curve = line;统一处理所有曲线。
抽象类与接口
// 抽象类:不能实例化,可包含实现
public abstract class Shape
{
public abstract double GetArea(); // 抽象方法(无实现,子类必须 override)
public string Description => $"面积:{GetArea():F2}"; // 可包含具体实现
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double GetArea() => Width * Height;
}
// 接口:纯契约,无实现
public interface IDrawable
{
void Draw();
bool IsVisible { get; set; }
}
public class Canvas : IDrawable // 一个类可实现多个接口
{
public bool IsVisible { get; set; } = true;
public void Draw()
{
Console.WriteLine("画布渲染");
}
}| 对比 | abstract class | interface |
|---|---|---|
| 实例化 | 不可 | 不可 |
| 包含实现 | 可以 | 不可(C# 8+ 可有默认实现) |
| 继承数量 | 单继承 | 可实现多个 |
| 构造函数 | 可以有 | 不可以 |
| 典型用途 | “是什么” | “能做什么” |
💡 AutoCAD 和 Revit API 大量使用抽象基类(
Curve、DBObject、Element),但你很少需要自定义抽象类。接口在定义命令时常用——为每个命令实现同一个接口。
访问修饰符
| 修饰符 | 访问范围 |
|---|---|
public |
任何代码均可访问 |
private |
仅当前类内部访问 |
protected |
当前类 + 派生类 |
internal |
当前程序集(.dll)内 |
protected internal |
当前程序集 或 派生类 |
private protected |
当前程序集内的派生类 |
🚨 AutoCAD/Revit 插件的入口命令类必须是
public,否则宿主程序无法通过反射发现。
静态成员
public class MathUtils
{
// 静态字段(整个程序共享一份)
public static readonly double PI = 3.14159265358979;
// 静态构造函数(在第一次使用类之前执行一次)
static MathUtils()
{
Console.WriteLine("MathUtils 初始化");
}
// 静态方法
public static double Square(double x) => x * x;
}
// 调用
double result = MathUtils.Square(5);扩展方法
无法修改已有类型时,为其"追加"方法:
// 必须是静态类中的静态方法,第一个参数用 this 修饰
public static class StringExtensions
{
public static bool IsNullOrEmpty(this string value)
=> string.IsNullOrEmpty(value);
public static string Truncate(this string value, int maxLength)
=> value.Length <= maxLength ? value : value[..maxLength] + "...";
}
// 使用方式与实例方法完全相同
string s = "Hello World";
bool empty = s.IsNullOrEmpty();
string shortStr = s.Truncate(5); // "Hello..."💡 AutoLISP 风格的工具函数在 AutoCAD .NET 开发中常以静态帮助类 + 扩展方法的形式组织,如
Point3d扩展方法。
密封类
public sealed class FinalClass // 不能被继承
{
// 密封类的方法调用无需虚方法查找,微优性能
}⚡ 除非有明确继承需求,否则 AutoCAD/Revit 的 DTO(数据传输对象)可以设计为
sealed。
泛型
// 泛型类
public class Repository<T>
{
private List<T> _items = new();
public void Add(T item) => _items.Add(item);
public T? Get(int index) => index < _items.Count ? _items[index] : default;
}
// 泛型约束
public class EntityRepo<T> where T : Entity
{
public void DrawAll(List<T> items)
{
foreach (var item in items)
item.Draw(); // 因为约束了 Entity,可以调用 Draw()
}
}
// 泛型方法
public static T? FirstOrDefault<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
foreach (var item in source)
if (predicate(item))
return item;
return default;
}🔬 泛型在编译时为每种值类型生成特化代码,引用类型共享同一份。这意味
List<int>和List<double>各有独立的机器码,但没有装箱开销。
委托与事件
委托(Delegate)和事件(Event)是 C# 实现回调、通知和松耦合的基础机制。对于新手来说,这是最容易被略过但实际开发中最常用的知识点。
为什么需要委托?—— 从动机讲起
假设你正在写一个过滤墙体的通用方法:
// 需求 1:找出所有长度超过 5m 的墙(硬编码条件)
public List<Wall> FindLongWalls(List<Wall> walls)
{
var result = new List<Wall>();
foreach (var wall in walls)
if (wall.Length > 5.0)
result.Add(wall);
return result;
}
// 需求 2:找出所有特定类型的墙(又要写一个新方法!)
public List<Wall> FindWallsByType(List<Wall> walls, string type)
{
var result = new List<Wall>();
foreach (var wall in walls)
if (wall.Type == type)
result.Add(wall);
return result;
}
// 需求 3:找出所有位于某楼层的墙...(无穷无尽的新方法 😰)所有方法的区别仅在于 if 里的那一个条件。委托让我们把"判断逻辑"本身作为参数传入:
// ✅ 一个通用方法,条件由调用者以委托形式传入
public List<Wall> FindWalls(List<Wall> walls, Func<Wall, bool> predicate)
{
var result = new List<Wall>();
foreach (var wall in walls)
if (predicate(wall)) // 执行传入的判断逻辑
result.Add(wall);
return result;
}
// 使用:传入什么条件就按什么条件过滤
var longWalls = FindWalls(walls, w => w.Length > 5.0);
var typeWalls = FindWalls(walls, w => w.Type == "剪力墙");
var levelWalls = FindWalls(walls, w => w.Level == "F2");🔬 委托的本质:“把方法当作参数传递”。在其他语言中你可能见过"函数指针"或"回调函数"——委托就是 C# 对这些概念的类型安全实现。
委托的定义与使用
自定义委托类型(传统方式)
// 步骤 1:定义委托类型(描述"什么样"的方法可以被传递)
public delegate double MathOperation(double x, double y);
// ↑返回类型 ↑委托名 ↑参数列表
// 步骤 2:编写符合委托签名的方法
public static double Add(double a, double b) => a + b;
public static double Multiply(double a, double b) => a * b;
// 步骤 3:将方法赋值给委托变量
MathOperation op = Add; // 把 Add 方法"装入"委托
double result = op(3, 5); // 通过委托调用 → 8
op = Multiply; // 换成 Multiply
result = op(3, 5); // → 15💡 委托变量就像"方法的容器"——你可以把一个符合签名的方法放进去,之后通过这个容器调用它。赋值时只写方法名不带括号(
Add不是Add()),因为你要传的是"方法本身"而非"调用结果"。
内置委托:Action、Func、Predicate
90% 的场景不需要自定义委托类型,用内置泛型委托即可:
// Func<参数1, 参数2, ..., 返回值> —— 有返回值
Func<int, int, int> add = (a, b) => a + b; // 两个 int 参数,返回 int
Func<string, bool> isEmpty = s => string.IsNullOrEmpty(s); // 一个 string,返回 bool
Func<bool> isReady = () => true; // 无参数,返回 bool
// Action<参数1, 参数2, ...> —— 无返回值(纯副作用)
Action<string> log = msg => Console.WriteLine(msg);
Action lineBreak = () => Console.WriteLine(); // 无参数也无返回值
// Predicate<T> —— 等价于 Func<T, bool>,专门用于判断
Predicate<int> isPositive = n => n > 0;| 类型 | 有返回值? | 典型用途 |
|---|---|---|
Func<T1, T2, ..., TResult> |
有(最后一个类型参数是返回值) | 变换、计算、判断 |
Action<T1, T2, ...> |
无(void) | 日志、写数据库、副作用操作 |
Predicate<T> |
bool | 过滤条件(等价 Func<T, bool>) |
多播委托 —— 一个委托串起多个方法
// 委托可以"串联"多个方法:调用时按添加顺序依次执行
Action<string> logger = Console.WriteLine;
logger += msg => File.AppendAllText("log.txt", msg + "\n"); // 追加一个方法
logger += msg => Debug.Write(msg); // 再追加
logger("Hello"); // 一次调用,三个方法依次执行!
// 输出:
// 控制台打印 "Hello"
// 写入文件 "Hello\n"
// Debug.Write("Hello")
// 移除方法
logger -= Console.WriteLine; // 取消订阅🔬 多播委托内部维护了一个调用链表(invocation list),
+=在尾部追加,-=查找并移除。如果-=的方法不存在于链表中,静默忽略(不报错)。
// 有返回值的多播委托:只返回最后一个方法的返回值!
Func<int, int> pipeline = x => x + 1;
pipeline += x => x * 10;
pipeline += x => x - 100;
int result = pipeline(5); // 只返回 -95(即 5 - 100)
// ⚠ 前两个方法的返回值被丢弃!🚨 多播委托有返回值时,只有最后一个方法的结果被返回。如果你需要收集所有返回值,需要手动遍历
GetInvocationList()。
从委托到 Lambda 表达式
// 写法演进——从最冗长到最简洁:
// 方式 1:命名方法
bool IsLong(Wall w) { return w.Length > 5.0; }
var r1 = FindWalls(walls, IsLong);
// 方式 2:匿名方法(C# 2.0)
var r2 = FindWalls(walls, delegate(Wall w) { return w.Length > 5.0; });
// 方式 3:Lambda 表达式(C# 3.0+)—— 最常用
var r3 = FindWalls(walls, (Wall w) => { return w.Length > 5.0; });
// 方式 4:Lambda 简化 —— 省略类型和花括号
var r4 = FindWalls(walls, w => w.Length > 5.0);
// 方式 5:方法组转换 —— 方法名直接当委托
var r5 = FindWalls(walls, IsLong); // 编译器自动推断Lambda 语法速查:
// 无参数
Func<string> f1 = () => "hello";
// 一个参数(括号可省略)
Func<int, int> f2 = x => x * 2;
// 等价于: Func<int, int> f2 = (x) => x * 2;
// 多个参数
Func<int, int, int> f3 = (a, b) => a + b;
// 多行语句体(用花括号 + return)
Func<int, int> f4 = x =>
{
int doubled = x * 2;
return doubled + 1;
};事件 —— 受控的委托
如果你在类中暴露一个公开的委托字段,外部代码可以做三件事:订阅、取消订阅,以及——直接调用或清空:
// ❌ 公开委托字段:外部可以随意调用和覆盖
public class Dangerous
{
public Action<string>? OnSomething; // 公开字段
}
var d = new Dangerous();
d.OnSomething = null; // 💥 恶意清除所有订阅者
d.OnSomething("hacked"); // 💥 外部随意触发event 关键字解决了这个问题:只允许外部 += 和 -=,不允许外部调用和赋值:
// ✅ 事件:外部只能订阅/取消订阅
public class SafePublisher
{
public event Action<string>? OnSomething; // 加 event 关键字
public void DoWork()
{
// 类内部可以触发事件
OnSomething?.Invoke("工作完成"); // ?. 保证无订阅者时不崩溃
}
}
var sp = new SafePublisher();
sp.OnSomething += msg => Console.WriteLine(msg); // ✅ 订阅
sp.OnSomething -= msg => Console.WriteLine(msg); // ✅ 取消订阅
// sp.OnSomething?.Invoke(""); // ❌ 编译错误!外部无法触发
// sp.OnSomething = null; // ❌ 编译错误!外部无法赋值💡 记忆口诀:委托是"方法的变量",事件是"委托的安全套"。公开字段 → 任何人都能赋值/调用;加
event→ 外部只能+=/-=。
标准事件模式:EventHandler
C# 有一套约定俗成的事件签名规范,.NET 框架和 AutoCAD/Revit API 都遵循它:
// 标准模式:委托签名为 (object sender, TEventArgs args)
// 第一个参数是触发事件的对象(sender)
// 第二个参数是事件数据
// 步骤 1:定义事件参数类(通常继承 EventArgs)
public class WallCreatedEventArgs : EventArgs
{
public Wall Wall { get; }
public DateTime Timestamp { get; }
public WallCreatedEventArgs(Wall wall)
{
Wall = wall;
Timestamp = DateTime.Now;
}
}
// 步骤 2:在发布者类中声明事件
public class WallManager
{
// EventHandler<T> 是内置泛型委托:void(object sender, T args)
public event EventHandler<WallCreatedEventArgs>? WallCreated;
// 不带自定义参数的简单事件
public event EventHandler? AllWallsProcessed;
// 步骤 3:在合适时机触发事件(命名为 OnXxx 是约定)
protected virtual void OnWallCreated(Wall wall)
{
WallCreated?.Invoke(this, new WallCreatedEventArgs(wall));
}
public void CreateWall()
{
var wall = new Wall();
// ...创建逻辑...
OnWallCreated(wall); // 通知所有订阅者
}
}
// 步骤 4:订阅者注册回调
var manager = new WallManager();
manager.WallCreated += (sender, args) =>
{
Console.WriteLine($"新墙创建:{args.Wall.Id},时间:{args.Timestamp}");
};常见的事件声明形式:
// 无数据
public event EventHandler? Click;
// 自定义数据
public event EventHandler<MyEventArgs>? DataReceived;
// 用 Action(非标准,但在简单场景中方便)
public event Action? Completed; // 无参传递
public event Action<string, int>? ProgressChanged; // 自定义参数🚨 用
Action作事件定义语法更简洁,但违背了 .NET 的设计规范(缺少sender)。在 AutoCAD/Revit 插件内部的简单场景中可以接受,但如果设计公共 API 给别人用,请遵循EventHandler<T>模式。
完整示例:AutoCAD 文档变更监听
下面是一个完整的插件事件订阅模式,也是 AutoCAD 开发中最常见的套路:
public class MyAutoCADPlugin : IExtensionApplication
{
// Revit 用 IExternalApplication,AutoCAD 用 IExtensionApplication
// 原理相同:在 Initialize 中订阅事件,在 Terminate 中取消
public void Initialize()
{
// 订阅文档创建事件
Application.DocumentManager.DocumentCreated += OnDocumentCreated;
// 订阅命令结束事件(带 sender 和 args 的标准模式)
Application.DocumentManager.MdiActiveDocument?.CommandEnded += OnCommandEnded;
// 订阅数据库保存事件
Application.DocumentManager.DocumentLockModeChanged += OnLockModeChanged;
}
public void Terminate()
{
// ⚠ 务必取消订阅!否则:
// 1. AutoCAD 关闭时可能引用已卸载的 DLL → 崩溃
// 2. 旧 DLL 无法被 GC 回收 → 内存泄漏
Application.DocumentManager.DocumentCreated -= OnDocumentCreated;
Application.DocumentManager.MdiActiveDocument.CommandEnded -= OnCommandEnded;
}
private void OnDocumentCreated(object sender, DocumentCollectionEventArgs e)
{
Document doc = e.Document;
doc.Editor.WriteMessage($"\n新文档已创建:{doc.Name}");
}
private void OnCommandEnded(object sender, CommandEventArgs e)
{
// sender 是触发该事件的 Document 对象
if (sender is Document doc)
{
doc.Editor.WriteMessage($"\n命令已完成:{e.GlobalCommandName}");
}
}
}常见陷阱
| 陷阱 | 说明 |
|---|---|
| 忘记取消订阅 | 事件源持有订阅者的引用 → 订阅者无法被 GC 回收 → 内存泄漏 |
| 事件在错误线程触发 | 在 AutoCAD/Revit 中,部分事件在后台线程触发,API 调用会崩溃 |
| null 检查遗漏 | 无订阅者时 event.Invoke() 空引用崩溃;用 ?.Invoke() |
| Lambda 闭包捕获循环变量 | for 循环中订阅事件,Lambda 捕获的变量是所有迭代共享的 |
| 多播委托返回值丢失 | 多播 Func 只返回最后一个结果 |
// 🚨 陷阱示例:Lambda 闭包
for (int i = 0; i < 5; i++)
{
button.Click += (s, e) => Console.WriteLine(i);
}
// 点击任何按钮都输出 5!i 是同一个变量
// ✅ 修复:复制到局部变量
for (int i = 0; i < 5; i++)
{
int captured = i;
button.Click += (s, e) => Console.WriteLine(captured);
}一句话总结
| 概念 | 一句话 |
|---|---|
| 委托 | 把方法当参数传(类型安全的函数指针) |
| Func | 有返回值的委托 |
| Action | 无返回值的委托 |
| Lambda | 短小的匿名方法,x => x * 2 |
| 事件 | 被 event 保护的委托,外部只能 +=/-= |
| EventHandler | 标准事件签名约定 (object sender, T args) |