简单的 C# 属性系统
本文演示了一个简单的属性系统的 C# 实现。
目录
简介
此属性系统示例实现基本上是一个属性包,具有一些管理功能,可以通过函数覆盖值,并有可能用于组合具有继承的实体系统,但它也可以用作消息总线、组件的通用基类等等。
背景
属性系统、对象系统、实体系统或任何其他此类“功能和结构”的名称都非常方便,可以处理异构数据并分离关注点。
我对此主题进行了大量阅读,在尝试比我可能说得更清楚地重复别人的话之前,我建议研究参考资料。
然而,似乎全世界都同意对某种可以取代巨大类层次结构的需求,甚至微软也为此提供了很多主题;例如,DependencyProperty 和 C# 中新的dynamic 关键字。
此处提供的示例是 dynamic 关键字和一些相关内容的替代品,并且有可能成为依赖属性的替代品。它不是组件系统,但可以用来创建组件系统。
本文重点在于构建一个能够存储任何类型数据的属性系统,并提供“覆盖”特定属性的实际值(通过返回该值的函数)的可能性。这使得可以轻松地在不同属性或跨属性系统之间建立关系,这可能是与其它属性包实现相比的主要优势。
避免使用新的 dynamic 关键字是一个设计目标,同时为类似依赖属性的结构提供更好的基础。XML 的序列化/反序列化不属于此示例,已被完全省略,正如线程安全和性能问题一样。然而,其中一些问题可能会在后续文章中介绍。
属性系统可以直接使用,也可以封装在专用类中。它们对于组合通用对象、构建组件容器、充当总线系统以及更多用途非常方便。
提供的代码包括一个用于直接使用的测试用例,以及一个展示通用对象示例的测试用例。
尽管有可能提供事件引用,但这些(尚未)在此文章中涵盖,但已包含在源代码中。可以在此处找到有关如何使用接口引用事件的技巧/窍门:www.codeproject.com/Tips/454172/Referencing-events-in-Csharp-using-interfaces-A-si [^]。
希望您喜欢。
接口概述
以下是对属性系统整体主要接口的简要概述
属性系统接口
属性系统定义了一个键类型 (K),提供了一个创建包的工厂方法,并允许使用定义。
public interface IPropertySystem<K> where K : IComparable, IFormattable, IConvertible, IComparable<K>, IEquatable<K>
{
IPropertyBag<K> CreateBag();
event Action<IPropertyDef<K> AfterDefinitionAdded;
event Action<IPropertyDef<K> AfterDefinitionRemoved;
IPropertyDef<K> GetDef(K propId);
void SetDefs(IPropertyDefs<K> defs);
}
属性定义接口
属性定义定义了与系统相同的键类型,提供了一个创建定义的工厂方法,并允许使用定义。
public interface IPropertyDefs<K> where K : IComparable, IFormattable,
IConvertible, IComparable<K>, IEquatable<K>
{
// factory
IPropertyDef<K> CreateDef(K id, Type type);
event Action<IPropertyDef<K> AfterDefinitionAdded;
event Action<IPropertyDef<K> AfterDefinitionRemoved;
void SetDefs(List<IPropertyDef<K> newDefs);
void AddDefs(List<IPropertyDef<K> newDefs);
void AddDef(IPropertyDef<K> newDef);
void RemoveDef(K propertyId);
void RemoveDef(IPropertyDef<K> def);
IPropertyDef<K> GetDef(K propertyId);
List<IPropertyDef<K> Defs { get; }
}
属性定义接口
属性定义定义了与系统相同的键类型,并且是 Id 和 ValueType 值的容器。
public interface IPropertyDef<K> where K : IComparable, IFormattable, IConvertible, IComparable<K>, IEquatable<K>
{
K Id { get; set; }
Type ValueType { get; set; }
}
属性包接口
属性包类定义了与系统相同的键类型,并允许创建/修改/删除属性值。
public interface IPropertyBag<K> where K: IComparable,
IFormattable, IConvertible, IComparable<K>, IEquatable<K>
{
event Func<K, object, bool> BeforeFuncAdd;
event Action<K, object> AfterFuncAdd;
event Action<K, object> AfterFuncAddFailed;
event Func<K, object, bool> BeforeFuncSet;
event Action<K, object> AfterFuncSet;
event Action<K, object> AfterFuncSetFailed;
event Func<K, object, bool> BeforeValueAdd;
event Action<K, object> AfterValueAdd;
event Action<K, object> AfterValueAddFailed;
event Func<K, object, bool> BeforeValueSet;
event Action<K, object> AfterValueSet;
event Action<K, object> AfterValueSetFailed;
event Func<K, bool> BeforeRemove;
event Action<K> AfterRemove;
bool TryGet(K propId, out object retValue);
bool TryGetFunc(K propId, out object retValue);
bool TryGetValue<T>(K propId, out T retValue);
T GetValue<T>(K propId, T defaultValue);
T GetValue<T>(K propId, Action<T> nonNullAction);
T GetValue<T>(K propId, Action<T> nonNullAction, Action nullAction);
R GetValue<T, R>(K propId, Func<T, R> nonNullFunc, R defaultValue);
R GetValue<T, R>(K propId, Func<T, R> nonNullFunc, Func<R> nullFunc);
void Set(K propId, object value);
void SetFunc(K propId, object value);
void SetValue<T>(K propId, T value);
bool TryRemove(K propId);
}
魔法在哪里发生
接下来,将展示系统中一些更令人感兴趣的代码片段。
实际上,不多。大部分魔法发生在 PropertyBag<K>
的 TryGetValue<T>
方法中。
框架通过一些案例判断来直接布局。
public bool TryGetValue<T>(K propId, out T retValue)
{
if (properties.ContainsKey(propId))
{
object p = properties[propId];
if (p == null)
{
// todo: handle property null value
}
else
{
// todo: handle property non-null value
}
}
else
{
// todo: handle property not set
}
}
没有设置属性是最简单的情况——返回该类型的默认值,并返回 false 似乎足够了。
// handle property not set
retValue = default(T);
return false;
如果属性存储的值为 null,我们必须检查该属性的类型是否不是值类型,并在需要时引发异常,因为值类型不喜欢 null。
// handle property null value
Type Ttype = typeof(T);
if (!Ttype.IsValueType)
{
retValue = default(T);
return true;
}
else
throw new Exception(string.Format("Value types don't support null"));
最后一步,也许是最有趣的部分(尽管它不是什么高科技),就是为了确定属性是否被函数覆盖而进行的类型魔法。
方法很简单:系统中的属性定义告诉我们预期的类型。如果包含的类型与预期的不同,我们只需检查它是否是返回预期类型的函数。如果不是,我们就惨了,会抛出异常。
// handle property non-null value
Type t = p.GetType();
IPropertyDef<K> pd = system.GetDef(propId);
Type vt = pd.ValueType;
bool b = vt.IsAssignableFrom(t);
if (b) // same type ^= no func
retValue = (T)p;
else
{
Func<T> f = p as Func<T>
if (f == null)
throw new Exception(string.Format("expected Func<{0}> but got {1}", typeof(T).ToString(), t.ToString()));
else
retValue = f();
}
return true;
最后,是该方法的全部代码。
public bool TryGetValue<T>(K propId, out T retValue)
{
if (properties.ContainsKey(propId))
{
object p = properties[propId];
if (p == null)
{
Type Ttype = typeof(T);
if (!Ttype.IsValueType)
{
retValue = default(T);
return true;
}
else
throw new Exception(string.Format("Value types don't support null"));
}
else
{
Type t = p.GetType();
IPropertyDef<K> pd = system.GetDef(propId);
Type vt = pd.ValueType;
bool b = vt.IsAssignableFrom(t);
if (b) // same type ^= no func
retValue = (T)p;
else
{
Func<T> f = p as Func<T>
if (f == null)
throw new Exception(string.Format("expected Func<{0}> but got {1}", typeof(T).ToString(), t.ToString()));
else
retValue = f();
}
return true;
}
}
else
{
retValue = default(T);
return false;
}
}
使用代码动态管理属性系统
以下段落展示了如何动态创建属性系统以及一些基本用例。
创建系统实例
属性系统实例至少需要一个键类型。虽然可以在构造系统实例之前设置属性定义,但在此示例中将系统和定义创建分开是有意义的。
// Create an instance of a property system with a string key type
PropertySystem<string> ps = new PropertySystem<string>();
创建属性定义供属性系统稍后使用
为了使属性系统能够区分值和返回值的函数,需要设置合适的定义。
// Create a container for the definitions
PropertyDefs<string> pd = new PropertyDefs<string>();
// and add some definitions; here an integer property named (= a key with the value of) "test-int"
pd.AddDef(pd.CreateDef("test-int", typeof(int)));
// add a new definition for a parameterless function returning a boolean value and which is named "a func returning a boolean"
pd.AddDef(pd.CreateDef("a func returning a boolean", typeof(Func<bool>)));
将属性定义分配给属性系统实例
属性系统实例需要了解其可能的属性,因为其创建的包会利用这些信息来区分值类型和函数类型。
// assign the definitions to the property system instance
ps.SetDefs(pd);
从属性系统实例创建属性包实例
属性系统可以被视为一个类定义(尽管这种比较具有误导性),属性包可以被视为一个实例[属性系统作为类的实例](尽管这种比较也具有误导性)。
// create a bag
PropertyBag<string> pb = ps.CreateBag() as PropertyBag<string>;
使用属性包实例
现在一切都已设置好,我们可以开始使用了。
因为包还是空的,所以我们先填入一些数据。
pb.SetValue("test-int", 42);
pb.SetValue("a func returning a boolean", new Func<bool>(() => true));
从包中取回数据非常简单。由于泛型类型推断,如果我们直接存储它们,甚至不需要显式指定返回类型。
bool ret;
int iValue;
ret = pb.TryGetValue("test-int", out iValue);
Func<bool> fBool;
ret = pb.TryGetValue("a func returning a boolean", out fBool);
然而,有一些方便的方法可以实现更流畅的使用。
如果可以提供默认值,则无需使用表示获取操作成功与否的返回值。
iValue = pb.GetValue("test-int", 0815);
通过函数进行链式调用也是可能的,但是,编译器此时需要类型提示。
pb.GetValue<int>("test-int", v => Debug.WriteLine(string.Format("successfully got {0}", v)), () => Debug.WriteLine("GetValue failed"));
转换也是可能的;这里,我们的 int
被返回为 string
。
pb.GetValue<int, string>("test-int", v => v.ToString(), () => "GetValue failed");
请记住,我们将“test-int”定义为 typeof(int)
,将“a func returning a boolean”定义为 typeof(Func<bool>)
,并且有可能用返回我们为该属性定义的类型的相应函数来覆盖任何属性值。
pb.SetValue("test-int", new Func<int>(() => 42));
pb.SetValue("a func returning a boolean", new Func<Func<bool>>(() => new Func<bool>(() => true)));
获取值的方式与之前相同,因为接口是透明的。然而,有时您可能知道某个值属性背后有一个函数。要直接访问它,包中还有另一个方法。
object o;
ret = pb.TryGetFunc("test-int", out o);
ret = pb.TryGetFunc("a func returning a boolean", out o);
使用代码创建基于属性包的对象
以下段落展示了如何创建基于属性包的对象。
由于这个例子被广泛使用且易于理解,我们将创建一个两栖车辆的通用定义。
为了简单起见,将只有一个方法:Accelerate
。
定义属性 ID
即使系统的键类型是 string
,但有时拥有预备常量以避免在源代码中充斥着魔术数字也是非常方便的。
由于我们在示例中使用了整数作为键,所以我毫不犹豫地为属性 ID 定义了合适的常量。
由于它们使用频率很高,所以将容器名称设置得简短是有意义的。
abstract class Ipsc
{
// properties
public const int Id = 0;
public const int MaxDriveSpeed = 1;
public const int MaxFlySpeed = 2;
public const int MaxSwimSpeed = 3;
public const int CurrentMedium = 4;
public const int CurrentSpeed = 5;
// actions / functions
public const int Accelerate = 10;
// todo: events
}
定义一个专业的属性系统
系统定义可以是原始 PropertySystem
的派生。我们将通过聚合来处理它,并仅实现 IPropertySystem
接口。出于性能原因,键类型定义为整数。
class IntKeyPropertySystem : IPropertySystem<int>
{
IPropertySystem<int> propSys;
public IntKeyPropertySystem()
: base()
{
// todo: create and define the property system
}
// todo: provide interface implementations
}
下一步是实现所有必需的接口。在此示例中,将它们代理到基对象就足够了。
// interface implementations
public event Action<IPropertyDef<int>> AfterDefinitionAdded;
public event Action<IPropertyDef<int>> AfterDefinitionRemoved;
public void SetDefs(IPropertyDefs<int> defs)
{
propSys.SetDefs(defs);
}
public IPropertyBag<int> CreateBag()
{
return propSys.CreateBag();
}
public IPropertyDef<int> GetDef(int propId)
{
return propSys.GetDef(propId);
}
我们不能忘记创建系统并设置事件路由。
public IntKeyPropertySystem() : IPropertySystem<int>
: base()
{
// create the property system
PropertySystemFactory psFactory = new PropertySystemFactory();
propSys = psFactory.Create<int>();
// todo: define properties
// properly route the events
propSys.AfterDefinitionAdded += new Action<IPropertyDef<int>>(propSys_AfterDefinitionAdded);
propSys.AfterDefinitionRemoved += new Action<IPropertyDef<int>>(propSys_AfterDefinitionRemoved);
}
void propSys_AfterDefinitionAdded(IPropertyDef<int> def)
{
if (AfterDefinitionAdded != null)
AfterDefinitionAdded(def);
}
void propSys_AfterDefinitionRemoved(IPropertyDef<int> def)
{
if (AfterDefinitionRemoved != null)
AfterDefinitionRemoved(def);
}
最后,我们定义可用的属性并将它们分配给系统。
// define properties
IPropertyDefs<int> defs = psFactory.CreateDefs<int>();
defs.SetDefs(new List<IPropertyDef<int>>{
{ defs.CreateDef(Ipsc.Id, typeof(int)) },
{ defs.CreateDef(Ipsc.MaxDriveSpeed, typeof(double)) },
{ defs.CreateDef(Ipsc.MaxFlySpeed, typeof(double)) },
{ defs.CreateDef(Ipsc.MaxSwimSpeed, typeof(double)) },
{ defs.CreateDef(Ipsc.CurrentMedium, typeof(Medium)) },
{ defs.CreateDef(Ipsc.CurrentSpeed, typeof(double)) },
{ defs.CreateDef(Ipsc.Accelerate, typeof(Action<double>)) }
// todo: define event properties
});
propSys.SetDefs(defs);
创建示例对象定义
示例对象拥有共享包的引用,一个全局唯一的对象 ID,并提供了一些代理访问器。
共享包在构造期间简单设置。
class ExampleObject
{
IPropertyBag<int> pbag;
// todo: add id management
public ExampleObject(IPropertyBag<int> pbag)
{
this.pbag = pbag;
// todo: add id management
}
// todo: add accessor methods
}
UID 管理也保持简单,目前仅用于参考和更轻松的调试;然而,出于多种原因,拥有对象 UID 常常是有意义的。
class ExampleObject
{
IPropertyBag<int> pbag;
// id management
static int nextId = 0;
public ExampleObject(IPropertyBag<int> pbag)
{
this.pbag = pbag;
// id management
this.pbag.SetValue(Ipsc.Id, nextId);
++nextId;
}
// todo: add accessor methods
}
最后,我们定义访问器方法。它们可以与包中的访问器完全不同,但在最简单的情况下,我们可以仅代理它们。
class ExampleObject
{
static int nextId = 0;
IPropertyBag<int> pbag;
public ExampleObject(IPropertyBag<int> pbag)
{
this.pbag = pbag;
this.pbag.SetValue(Ipsc.Id, nextId);
++nextId;
}
// proxies
public bool TryGetFunc(int propId, out object retValue)
{
return this.pbag.TryGetFunc(propId, out retValue);
}
public bool TryGetValue<T>(int propId, out T retValue)
{
return this.pbag.TryGetValue<T>(propId, out retValue);
}
public T GetValue<T>(int propId, T defaultValue)
{
return this.pbag.GetValue<T>(propId, defaultValue);
}
public T GetValue<T>(int propId, Action<T> nonNullAction)
{
return this.pbag.GetValue<T>(propId, nonNullAction);
}
public T GetValue<T>(int propId, Action<T> nonNullAction, Action nullAction)
{
return this.pbag.GetValue<T>(propId, nonNullAction, nullAction);
}
public R GetValue<T, R>(int propId, Func<T, R> nonNullFunc, R defaultValue)
{
return this.pbag.GetValue<T, R>(propId, nonNullFunc, defaultValue);
}
public R GetValue<T, R>(int propId, Func<T, R> nonNullFunc, Func<R> nullFunc)
{
return this.pbag.GetValue<T, R>(propId, nonNullFunc, nullFunc);
}
public void Set(int propId, object value)
{
this.pbag.Set(propId, value);
}
public void SetFunc(int propId, object value)
{
this.pbag.SetFunc(propId, value);
}
public void SetValue<T>(int propId, T value)
{
this.pbag.SetValue<T>(propId, value);
}
public bool TryRemove(int propId)
{
return this.pbag.TryRemove(propId);
}
}
创建示例系统管理器
到目前为止,我们已经定义了属性系统、其属性以及示例对象。但我们仍然遗漏了整个逻辑。
为了保持简单,所有剩余的逻辑都放在一个类中,该类也作为测试定义的容器。
class TestIntKeyPropertySystem
{
public void Run()
{
// todo
}
}
创建系统和一些示例对象实例非常简单。
public void Run()
{
// create an instance of our test property system
IntKeyPropertySystem ikps = new IntKeyPropertySystem();
// create two example objects, each with its own property bag
ExampleObject eo1 = new ExampleObject(ikps.CreateBag());
ExampleObject eo2 = new ExampleObject(ikps.CreateBag());
// todo: init the properties of the two example objects
// todo: execute test(s)
}
设置属性值也相当容易。
public void Run()
{
// [...]
// init the properties of the two example objects
// the approach for this example is to route everything through this class
const double maxDriveSpeedEo1 = 100;
const double maxSwimSpeed = 40;
eo1.SetValue(Ipsc.MaxDriveSpeed, maxDriveSpeedEo1);
eo1.SetValue(Ipsc.MaxFlySpeed, 300);
eo1.SetValue(Ipsc.MaxSwimSpeed, maxSwimSpeed);
eo1.SetValue(Ipsc.CurrentMedium, Medium.Ground);
eo1.SetValue(Ipsc.CurrentSpeed, 0);
eo2.SetValue(Ipsc.MaxDriveSpeed, 90);
eo2.SetValue(Ipsc.MaxFlySpeed, 250);
// we define the MaxSwimSpeed for the example object 2 as dependent on the swim speed of the example object 1
// with a default of 50 for the case when the example object 1 doesn't define its maximum swim speed
eo2.SetFunc(Ipsc.MaxSwimSpeed, new Func<double>(() => eo1.GetValue(Ipsc.MaxSwimSpeed, 50.0)));
eo2.SetValue(Ipsc.CurrentMedium, Medium.Water);
eo2.SetValue(Ipsc.CurrentSpeed, 0);
// todo: define "acceleration"
}
我们“对象”中唯一的“真实”方法是 Accelerate
。
public void Run()
{
// [...]
// define a shared acceleration function and "inject" the object context information
// one possibility to do so is the one below, where the context is added by defining lambdas appropriately
eo1.SetValue<Action<double>>(Ipsc.Accelerate, s => Accelerate(eo1, s));
eo2.SetValue<Action<double>>(Ipsc.Accelerate, s => Accelerate(eo2, s));
}
当然,我们必须实现 Accelerate
方法。
void Accelerate(ExampleObject eo, double speed)
{
Medium medium;
if (!eo.TryGetValue(Ipsc.CurrentMedium, out medium))
return;
int prop;
switch (medium)
{
case Medium.Ground:
prop = Ipsc.MaxDriveSpeed;
break;
case Medium.Air:
prop = Ipsc.MaxFlySpeed;
break;
case Medium.Water:
prop = Ipsc.MaxSwimSpeed;
break;
default:
return;
}
// todo: try to ask if the speed change may happen if needed and return if not
// if we've got a max speed value, limit the new speed accordingly
double maxSpeed;
if (eo.TryGetValue(prop, out maxSpeed))
speed = Math.Min(speed, maxSpeed);
eo.SetValue(Ipsc.CurrentSpeed, speed);
// todo: try to inform about the new speed if needed
}
限制
由于 SetValue
会引发值已更改事件,因此目前无法订阅被函数覆盖的属性的变化。
值得关注的点
这里提出的属性系统方法只是众多方法之一,目前不适用于生产环境。
静态类型检查是一个非常强大的机制,在运行时进行显然会引发一些性能和安全问题。
区分值属性及其函数等效项取决于运行时类型信息,这可以通过将信息放入属性或放入系统来替换。这也会很棒,因为它允许从包中删除对系统的反向引用。
只读属性已完全从实现中删除,但可以相当容易地添加它们,例如,通过将对象容器替换为更智能的容器,该容器为此提供属性,或者通过将管理委托给专门的包,该包以不同的方式保留此类信息 - 例如,使用单独的属性列表。
参考资料
- stackoverflow/Tomas Vana: Implementation of a general-purpose object structure (property bag) ^
- gamedev.net/JTippetts: Entity system ^
- west-wind.com/Rick Strahl's Web Log: An Xml Serializable PropertyBag Dictionary Class for .NET ^
- cowboyprogramming/Mick West: Evolve Your Hierarchy ^
- codeproject.com/Graham Harrison: Implementing a PropertyBag in C# ^
- www.academia.edu/401854/A_Generic_Data_Structure_for_An_Architectural_Design_Application ^
历史
- 2013/01/31:添加了指向“事件引用”技巧/窍门的链接,添加了内容参考链接,并修复了一些拼写错误。
- 2013/01/24:添加了参考资料。
- 2012/08/31:添加了限制以及对实现的解释。
- 2012/08/29:提交了第一个版本。