dotnet 写一个支持层层继承属性的对象

  • A+
所属分类:.NET技术
摘要

我最近在造一个比 Excel 差得多的表格控件,其中一个需求是属性的继承。大家都知道,表格里面有单元格,单元格里面允许放文本,文本可以放多段文本。本文的主角就是文本段落的样式属性,包括文本字体字号颜色等等属性。文本段落的属性,如果没有特别设置,将使用单元格里面的文本样式属性。而如果单元格里面,没有特别指定此单元格使用特殊的文本样式,将会继承使用当前所在的行的文本样式。如果当前行没有特殊指定文本样式属性,那么将会使用文档的默认样式。文档默认样式将会根据是否有特殊指定而采用主题样式

我最近在造一个比 Excel 差得多的表格控件,其中一个需求是属性的继承。大家都知道,表格里面有单元格,单元格里面允许放文本,文本可以放多段文本。本文的主角就是文本段落的样式属性,包括文本字体字号颜色等等属性。文本段落的属性,如果没有特别设置,将使用单元格里面的文本样式属性。而如果单元格里面,没有特别指定此单元格使用特殊的文本样式,将会继承使用当前所在的行的文本样式。如果当前行没有特殊指定文本样式属性,那么将会使用文档的默认样式。文档默认样式将会根据是否有特殊指定而采用主题样式

如此复杂的层层继承逻辑,如果每个属性都需要自己一层层去寻找,那代码量将会特别多。维护起来就想吃桌子

为了保住桌子,咱来写一个支持层层继承属性的对象。如在当前层找不到某个属性,将会往上一层自动去找,一层层找。如果都找不到,将返回默认值

以下是这个类的定义代码

    public class FlattenObject     {         /// <summary>         /// 创建带继承的对象         /// </summary>         /// <param name="reserved"></param>         public FlattenObject(FlattenObject? reserved = null)         {             Reserved = reserved;         }          private FlattenObject? Reserved { get; }         private Dictionary<string, object> ValueDictionary { get; } = new Dictionary<string, object>();          /// <summary>         /// 设置属性值         /// </summary>         /// <param name="value"></param>         /// <param name="propertyName"></param>         protected void SetValue(object value, [CallerMemberName] string propertyName = null!)         {             ValueDictionary[propertyName] = value;         }          /// <summary>         /// 获取属性值         /// </summary>         /// <typeparam name="T"></typeparam>         /// <param name="propertyName"></param>         /// <returns></returns>         protected T? GetValue<T>([CallerMemberName] string propertyName = null!)             => GetValue<T>(default!, propertyName);          /// <summary>         /// 获取属性值         /// </summary>         /// <typeparam name="T"></typeparam>         /// <param name="defaultValue"></param>         /// <param name="propertyName"></param>         /// <returns></returns>         protected T GetValue<T>(T defaultValue, [CallerMemberName] string propertyName = null!)         {             if (ValueDictionary.TryGetValue(propertyName, out var value))             {                 return (T) value;             }             else             {                 if (Reserved is not null)                 {                     return Reserved.GetValue<T>(defaultValue, propertyName);                 }                 else                 {                     return defaultValue;                 }             }         }     } 

通过 Reserved 属性表示的是当前层的上一层的对象,用于给当前层进行继承。因为每一层都包含了上一层的对象,因此从最下层就可以一层层自动找到属性的值

继承当前类型,即可写出下面代码

        class FooFlattenObject : FlattenObject         {             public FooFlattenObject(FlattenObject reserved = null) : base(reserved)             {             }              public string FontName             {                 set => SetValue(value);                 get => GetValue<string>();             }              public int Count             {                 set => SetValue(value);                 get => GetValue<int>();             }         } 

如上面代码,在各个属性的 set 和 get 都换成调用方法,而不需要定义字段

下面来尝试写单元测试

            "给定可继承的对象,可以从继承的对象拿到属性值".Test(() =>             {                 var reserved = new FooFlattenObject()                 {                     FontName = "微软雅黑"                 };                  var fakeFlattenObject = new FooFlattenObject(reserved);                 Assert.AreEqual("微软雅黑", fakeFlattenObject.FontName);                  fakeFlattenObject.Count = 2;                 Assert.AreEqual(2, fakeFlattenObject.Count);                 Assert.AreEqual(0, reserved.Count);             }); 

可以看到在 reserved 对象里面设置了 FontName 的值,可以被 fakeFlattenObject 继承拿到,同时自动读取的代码对于上层业务来说几乎没有

对 fakeFlattenObject 进行设置 Count 的值,不会影响到 reserved 对象

通过此方法可以让存在层层继承逻辑的代码不需要大量重复。除了在表格上使用,也可以用在如解析 PPT 的形状内文本,如 PPT 的图片裁剪等需要继承属性的逻辑上

上面的代码也存在不足,那就是对于结构体不友好,如 bool 或 int 等类型,都需要转换为 object 存放