如何用智能方法(含指针)替换C#数据仓库的样板代码
这个问题我之前也遇到过——大量重复的字段访问方法简直是维护噩梦!在C#里我们有几种方案可以解决,既可以用类型安全的优雅方式,也能实现你想要的基于指针偏移的模板方法,我来详细说说:
方案一:类型安全的泛型+委托(推荐)
最稳妥且易维护的方式是把重复的循环查找逻辑抽成通用方法,然后将字段的访问逻辑以委托的形式传入。这样既避免了样板代码,又完全符合C#的类型安全要求。
class Example { readonly MyItemDataList _myList = new MyItemDataList(); // 通用查找逻辑:找到对应ID的Item,再执行字段访问逻辑 private T GetField<T>(int id, Func<MyItem, T> fieldAccessor) { foreach (var item in _myList.Items) { if (item.Id == id) { return fieldAccessor(item); } } return default; // 实际项目中建议抛出NotFound异常,比返回默认值更合理 } // 每个字段的访问方法现在极简,只需要指定字段 public float GetSomeFloat(int id) => GetField(id, item => item.SomeFloat); public int GetSomeInt(int id) => GetField(id, item => item.SomeInt); // 新增字段时,只需要加一行类似的代码即可 }
这个方案的优势很明显:代码可读性高、类型安全、不需要特殊权限,而且委托调用的性能开销几乎可以忽略不计。
方案二:使用Unsafe代码实现指针偏移(符合你的思路)
如果你确实想通过内存偏移的方式实现,C#的unsafe代码支持直接操作结构体的内存地址,但需要注意几个关键限制:
第一步:固定结构体的内存布局
首先要确保你的MyItem结构体使用固定布局,这样字段的内存偏移量是确定的:
[Serializable] [StructLayout(LayoutKind.Sequential, Pack = 4)] // 指定顺序布局和内存对齐 struct MyItem { public readonly int Id; public int SomeInt; public float SomeFloat; // 注意:这里只能包含值类型,不能有字符串等引用类型 }
第二步:实现通用的偏移访问方法
接下来可以写一个通用的unsafe方法,通过字段偏移量直接读取内存中的值:
class Example { readonly MyItemDataList _myList = new MyItemDataList(); // 通用unsafe方法:根据ID和字段偏移量获取对应值 private unsafe T GetFieldByOffset<T>(int id, int fieldOffset) where T : unmanaged { foreach (var item in _myList.Items) { if (item.Id == id) { fixed (MyItem* pItem = &item) { // 计算字段的内存地址:结构体指针 + 偏移量 byte* pField = (byte*)pItem + fieldOffset; // 转换为目标类型指针并取值 return *(T*)pField; } } } return default; } // 提前计算每个字段的偏移量(只需要计算一次) private static readonly int SomeIntOffset = (int)Marshal.OffsetOf<MyItem>(nameof(MyItem.SomeInt)); private static readonly int SomeFloatOffset = (int)Marshal.OffsetOf<MyItem>(nameof(MyItem.SomeFloat)); // 字段访问方法 public int GetSomeInt(int id) => GetFieldByOffset<int>(id, SomeIntOffset); public float GetSomeFloat(int id) => GetFieldByOffset<float>(id, SomeFloatOffset); }
注意事项:
- 必须开启项目的允许不安全代码选项(在项目属性的「生成」标签中勾选)
- 仅支持
unmanaged类型(即纯值类型,不能包含引用类型字段) - 结构体的布局必须固定,不能使用默认的自动布局,否则偏移量可能会变化
- 这个方案性能和直接访问几乎一致,但可读性较差,且存在内存安全风险,除非有特殊性能需求,否则不推荐
额外优化:用字典提升查找性能
不管用哪种方案,每次循环遍历查找ID的效率都很低(O(n)复杂度)。如果你的数据量较大,建议提前把Items转换成字典,将查找复杂度降到O(1):
class Example { readonly Dictionary<int, MyItem> _itemById; public Example(MyItemDataList myList) { // 初始化时一次性构建ID到Item的映射 _itemById = myList.Items.ToDictionary(item => item.Id); } // 通用方法改成从字典直接取值 private T GetField<T>(int id, Func<MyItem, T> fieldAccessor) { if (_itemById.TryGetValue(id, out var item)) { return fieldAccessor(item); } return default; } // 字段访问方法保持不变 public float GetSomeFloat(int id) => GetField(id, item => item.SomeFloat); public int GetSomeInt(int id) => GetField(id, item => item.SomeInt); }
这个优化对性能的提升非常显著,尤其是数据量较大的时候,强烈建议加上。
内容的提问来源于stack exchange,提问作者Radoslaw Kozaczuk
相关产品推荐
相关产品推荐

