C#游戏引擎线性代数库:向量操作选硬编码还是循环?如何实现可维护代码?
问题:如何为不同维度的向量类定义点积这类重复方法,兼顾性能与可维护性?
我是一名业余开发者,正基于C#开发一个兴趣导向的长期项目——简易游戏引擎。项目初期需要实现线性代数数学库,目前在向量类的操作实现上遇到了两难:
现有两种实现方案
方案1:硬编码每个向量类的操作
class Vector2 { public float X { get; } public float Y { get; } // Operations public static float Dot(Vector2 a, Vector2 b) => a.X * b.X + a.Y * b.Y; // 其他操作... } class Vector3 { public float X { get; } public float Y { get; } public float Z { get; } // Operations public static float Dot(Vector3 a, Vector3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z; // 其他操作... }
- 优点:性能最优,适合游戏引擎的高频计算场景
- 缺点:代码冗余,所有向量操作(加法、标量乘法等)都要重复编写,易出错且维护成本高
方案2:抽象基类+索引器复用逻辑
abstract class Vector { // Indexer public float this[int index] { get => GetValueByIndex(index); } protected abstract float GetValueByIndex(int index); public abstract int Length { get; } // Operations public static float Dot(Vector a, Vector b) { float DotProduct = 0; for (int i = 0; i < a.Length; i++){ DotProduct += a[i] * b[i]; } return DotProduct; } // 其他操作... } class Vector2 : Vector { public float X { get; } public float Y { get; } public override int Length => 2; protected override float GetValueByIndex(int index) { if (index == 0) return X; if (index == 1) return Y; throw new ArgumentOutOfRangeException("index must be 0 or 1"); } } class Vector3 : Vector { public float X { get; } public float Y { get; } public float Z { get; } public override int Length => 3; protected override float GetValueByIndex(int index) { if (index == 0) return X; if (index == 1) return Y; if (index == 2) return Z; throw new ArgumentOutOfRangeException("index must be 0, 1 or 2"); } }
- 优点:代码复用性强,维护成本低
- 缺点:循环、虚拟调用和条件判断会带来性能损耗,不符合游戏引擎的性能要求
解答
两种方案的核心矛盾
硬编码方案的核心是性能优先但冗余,抽象基类方案是可维护性优先但性能差,游戏引擎场景下两者无法直接取舍,因此需要寻找兼顾的替代方案。
兼顾性能与可维护性的最优方案
1. C# 11+ 静态抽象接口(推荐)
利用C# 11引入的静态抽象接口成员,定义统一的向量操作契约,同时让每个向量类实现硬编码的操作,再通过泛型约束复用通用逻辑。
示例实现
// 定义向量操作的契约接口 public interface IVector<T> where T : IVector<T> { static abstract float Dot(T a, T b); static abstract T Add(T a, T b); static abstract T Multiply(T vector, float scalar); // 按需添加其他操作 } // Vector2 实现接口 public readonly struct Vector2 : IVector<Vector2> { public float X { get; } public float Y { get; } public Vector2(float x, float y) => (X, Y) = (x, y); public static float Dot(Vector2 a, Vector2 b) => a.X * b.X + a.Y * b.Y; public static Vector2 Add(Vector2 a, Vector2 b) => new(a.X + b.X, a.Y + b.Y); public static Vector2 Multiply(Vector2 vector, float scalar) => new(vector.X * scalar, vector.Y * scalar); } // Vector3 实现接口 public readonly struct Vector3 : IVector<Vector3> { public float X { get; } public float Y { get; } public float Z { get; } public Vector3(float x, float y, float z) => (X, Y, Z) = (x, y, z); public static float Dot(Vector3 a, Vector3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z; public static Vector3 Add(Vector3 a, Vector3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z); public static Vector3 Multiply(Vector3 vector, float scalar) => new(vector.X * scalar, vector.Y * scalar, vector.Z * scalar); } // 通用向量工具类,复用逻辑 public static class VectorUtilities<T> where T : IVector<T> { public static float Dot(T a, T b) => T.Dot(a, b); public static T Add(T a, T b) => T.Add(a, b); public static T Scale(T vector, float scalar) => T.Multiply(vector, scalar); }
- 优势:
- 性能和硬编码完全一致:JIT会直接内联静态抽象方法的调用,无额外开销
- 可维护性强:接口定义了统一契约,确保所有向量类实现必要操作,修改逻辑只需更新对应向量类的实现
- 支持值类型优化:使用
readonly struct避免GC分配,进一步提升性能
2. 代码生成(适合大量向量操作场景)
如果需要实现几十种向量操作,手动编写硬编码代码效率低,可以用T4模板或Roslyn源代码生成器自动生成不同维度向量类的所有操作。
比如编写一个T4模板,定义向量维度列表和操作模板,运行模板后自动生成Vector2、Vector3、Vector4的完整代码。
- 优势:完全保留硬编码的性能,只需维护模板代码,修改一次即可同步所有向量类
- 劣势:需要学习代码生成工具的使用,初期有学习成本
3. 避坑建议
- 绝对避免使用抽象基类方案:游戏引擎中向量操作是高频调用,循环和虚拟调用的性能损耗会被放大,严重影响帧率
- 始终使用值类型(struct):向量是值语义类型,用结构体避免GC分配,JIT对值类型的优化也远好于引用类型
内容的提问来源于stack exchange,提问作者NotARealProgrammer
相关产品推荐
相关产品推荐

