c# Attribute 使用约束

Attribute是C#中一个广泛使用的特性,通过使用Attribute对程序集中的类型进行标记,并通过Reflection实现一些特殊的需求,同时Attribute也可以在编译时储存一些静态信息。但是在使用Attribute时也有一些限制。

问题的提出

目前工作中主要是编写与维护一套在Unity中使用的Continuous Integration 框架。在这套框架中大量使用了Attribute来对集成的第三方SDK进行标记,来进行一些处理。最初的做法是一些SDK,通过约定静态方法,在CI的其他阶段进行调用

1
2
3
4
5
6
7
8
public class SDKA :SDKBase
{
public static void ModifyConfig(ref Hashtable config)
{
//SDK do modify
}
....
}

通过反射调用该方法的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
public void SDKsModifyConfig()
{
Type[] sdktypes = assembly.GetTypes();
//过滤所有的的SDK类型
...
foreach(var tsdk in sdktypes)
{
MethodInfo method = tsdk.GetMethod("ModifyConfig", BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic);

method.Invoke(null, config);
}
...
}

对于这种调用方式,不太好的地方是参与开发SDK的人员必须约定用于反射的方法名,以及该方法的参数。每次接入SDK编写一个SDK的类型都需重新编写,容易造成错误(error-prone)。

如果我们使用在基类定义方法,在子类覆盖的形式,这样可以避免发生错误,同时IDE的自动完成可以帮助我们完成代码。但是这样在反射的时候就需要有类的对象,而其实我们所需要的方法是针对与类的静态方法。

1
2
3
4
5
6
7
8
9
10
11
12
public class SDKBase
{
public virtual void ModifyConfig(ref Hashtable config)
{
}
}
public class SDKA :SDKBase
{
public override void ModifyConfig(ref Hashtable config)
{
}
}

使用Attribute

另一中解决方式是将这些方法定义在Attribute中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CustomSDKAttribute :Attribute
{
public void ModifyConfig(ref Hashtable config)
{
}
}
[CustomSDK]
public class SDKA :SDKBase
{
}

...
foreach(var type in sdktypes)
{
if(Attribute.IsDefined(type,typeof(CustomSDKAttribute)))
{
var attrs = type.GetCustomAttributes(typeof(CustomSDKAttribute), true);
CustomSDKAttribute sdkattr = attrs[0] as CustomSDKAttribute;
sdkattr.ModifyConfig(ref config);
}
}

将需要反射的方法放在Attribute的定义中后我们在反射了类获得Attribute之后可以直接调用该方法,不需要再反射方法了。

对于不同的SDK的类型需要不同的实现,我们可以定义一个Attribute的基类,通过Attribute的子类来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[CustomSDK]
public class SDKA :SDKBase
{
}
[CustomSDK_Impl]
public class SDKB:SDKBase
{
}
public class CustomSDKAttribute :Attribute
{
public virtual void ModifyConfig(ref Hashtable config)
{
}
}
public class CustomSDK_Impl :CustomSDKAttribute
{
public override void ModifyConfig(ref Hashtable config)
{
//覆盖基类的实现
}
}

Attribute的约束

我们知道在定义Attribute的时候可以给Attribute添加约束AttributeUsage

[AttributeUsage(AttributeTargets.Class,AllowMultiple =false)]

但是只能通过AttributeUsage约束Attribute添加到程序集对象的类型,并不能约束Attribute修饰的对象的继承链等其他特性。也就是不能使用泛型和where关键字。这样就会导致我们给SDK定义的Attribute可以被任意使用其他类型的对象上,这是我们不愿意看到的。

由于我们是使用反射来查找所有标记为该Attribute的类型,缺少了类型的继承链约束后,无疑会造成一些隐患。但是由于C#语言本身不提供Attribute的类型约束,在google之后通过一个非常trick的方式解决了Attribute的约束问题。

我们将Attribute的定义声明在需要约束的基类中,并且使用protected修饰符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SDKBase
{
public static void ModifyConfig(ref Hashtable config)
{
}

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
protected class CustomSDKAttribute : Attribute
{
public virtual void ModifyConfig(ref Hashtable config)
{

}
}
}

这样CustomSDKAttribute就只能在SDKBase的子类进行访问,下面的代码便会产生编译错误。

1
2
3
4
[CustomSDK]				//Error:未找到类型
public class TestClassA
{
}

这样就我们就相当与声明了where T:SDKBASE,对Attribute的使用进行了约束。

总结

使用Attribute定义的修饰符将Attribute修饰的对象加上约束。

对于将需要反射调用的方法存放在Attribute中还有一个好处是。在Unity开发中,如果我们只在Editor层面进行对这些方法的反射和调用,可以将该Attribute限定在编辑器环境,防止这部分代码污染最后编译的版本。

[System.Diagnostics.Conditional("UNITY_EDITOR")]

但是如果使用最初的反射方法的方式,这需要给所有的反射方法加上宏的限定,同时不一定可以使用System.Diagnostics.Conditional这个Attribute进行宏的约束。Conditional要求限定的方法必须是void的返回值。讲方法定义在Attribute中直接移除Attribute不受该约束限制