Skip to content
C#
面向对象

面向对象

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 继承自 EntityLineArcCirclePolyline 又继承自 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 大量使用抽象基类(CurveDBObjectElement),但你很少需要自定义抽象类。接口在定义命令时常用——为每个命令实现同一个接口。


访问修饰符

修饰符 访问范围
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)