作者:卡卡西0旗木

泰斗链接:http://www.taidous.com/forum.php?mod=viewthread&tid=32902&_dsign=df9be613

 

这篇文章主要讲一下C#里面Attribute的使用方法及其可能的应用场景。
比如你把玩家的血量、攻击、防御等属性写到枚举里面。然后界面可能有很多地方要根据这个枚举获取属性的描述文本。
比如你做网络框架的时候,一个协议号对应一个类的处理或者一个方法。
比如你做ORM,一个类的属性是否映射持久化文件中的属性,映射过去的属性名是什么。

1、什么是Attribute
如果用过Java的Annotation的同学,可以把Attribute当成Annotation来看待。
还不了解Attribute的同学不用急,我们看一下官方的解释:

Attribute class associates predefined system information or user-defined custom information with a target element. A target element can be an assembly, class, constructor, delegate, enum, event, field, interface, method, portable executable file module, parameter, property, return value, struct, or another attribute.
Information provided by an attribute is also known as metadata. Metadata can be examined at run time by your application to control how your program processes data, or before run time by external tools to control how your application itself is processed or maintained. For example, the .NET Framework predefines and uses attribute types to control run-time behavior, and some programming languages use attribute types to represent language features not directly supported by the .NET Framework common type system.
Attribute class. Attributes can be applied to any target element; multiple attributes can be applied to the same target element; and attributes can be inherited by an element derived from a target element. AttributeTargets class to specify the target element to which the attribute is applied.
Attribute class provides convenient methods to retrieve and test custom attributes. Applying Attributes and Extending Metadata Using Attributes.

翻译过来就是:

    Attribute类可以把目标元素和一个预定义的信息或者是用户自定义信息关联起来。这里的目标元素可以是assembly,class,constructor,delegate,enum,event,field,interface,method,可执行文件模块,parameter,property,return value,struct或其它的Attribute。
    Attribute提供的信息也被称为元数据(metadata)。元数据能用于在运行时控制怎样访问你的程序数据,或者在运行前通过额外的工具来控制怎样处理你的程序或部署它。例如.NET Framework预定义并使用attribute去控制运行时行为,一些编程语言使用attribute类型来描述.NET Framework中通用类型不直接支持的语言特性。
    所有的Attribute类型直接或间接从Attribute类继承。Attribute能应用到任何target元素;多个Attribute能应用到相同的元素;
    Attribute类提供遍历的方法去取出和测试自定义Attribute。更多关于Attribute的信息,可以看Applying Attributes和Extending Metadata Using Attributes。
如果你看了官方的解释不明白,看了我的翻译也不明白。也没事。。。我们接下来举个例子看看Attribute能做啥。

2、用Attribute将枚举和一个描述文本绑定在一起
假设有这个枚举

[C#] 纯文本查看 复制代码

public enum Properties
{
    /// <summary>
    /// 血量
    /// </summary>
    HP = 1,

    /// <summary>
    /// 物理攻击
    /// </summary>
    PhyAtk = 2,

    /// <summary>
    /// 物理防御
    /// </summary>
    PhyDef = 3,

    /// <summary>
    /// 法术攻击
    /// </summary>
    MagAtk = 4,

    /// <summary>
    /// 法术防御
    /// </summary>
    MagDef = 5
}

注意:如果含中文的代码编译报“Newline in constant”的错。那么请将文件的编码保存为“带BOM的UTF-8”。VS中可以在“文件”-“高级保存选项”,然后选择编码下拉中选择。

然后你现在想要根据枚举来获得中文描述:比如传入:

Properties.MagDef返回“法术防御”。
最原始的做法:

[C#] 纯文本查看 复制代码

public class PropertiesUtils
{
    public static string GetDescByProperties(Properties p)
    {
        switch (p)
        {
            case Properties.HP:
                return "血量";
            case Properties.PhyAtk:
                return "物理攻击";
            case Properties.PhyDef:
                return "物理防御";
            case Properties.MagAtk:
                return "法术攻击";
            case Properties.MagDef:
                return "法术防御";
            default:
                return "未知属性:" + p;
        }
    }
}

这样确实可以解决问题,但是我们可以用Attribute来做的更好。可以做的更好干嘛不呢?
先定义一个用于存储描述文本的Attribute。

[C#] 纯文本查看 复制代码

[System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Enum)]
public class PropertiesDesc : System.Attribute
{
    public string Desc { get; private set; }

}

没错,看起来是不是觉得很简单。
然后我们就可以把上面定义的PropertiesDesc加到Properties上面,像这样:

[C#] 纯文本查看 复制代码

public enum Properties
{

    [PropertiesDesc("血量")]
    HP = 1,

    [PropertiesDesc("物理攻击")]
    PhyAtk = 2,

    [PropertiesDesc("物理防御")]
    PhyDef = 3,

    [PropertiesDesc("法术攻击")]
    MagAtk = 4,

    [PropertiesDesc("法术防御")]
    MagDef = 5
}

OK。这样,我们相当于就把一个文本描述信息通过Attribute关联到我们的枚举属性了。
那么怎样获取?我们来重写之前的PropertiesUtils类。

[C#] 纯文本查看 复制代码

public class PropertiesUtils
{
    public static string GetDescByProperties(Properties p)
    {
        Type type = p.GetType();
        FieldInfo[] fields = type.GetFields();
        foreach (FieldInfo field in fields)
        {
            if (field.Name.Equals(p.ToString()))
            {
                object[] objs = field.GetCustomAttributes(typeof(PropertiesDesc), true);
                if (objs != null && objs.Length > 0)
                {
                    return ((PropertiesDesc)objs[0]).Desc;
                }
                else
                {
                    return p.ToString() + "没有附加PropertiesDesc信息";
                }
            }
        }
        return "No Such field : "+p;
    }
}

可以看到。这里面已经不用自己去判断哪个枚举值返回哪个字符串描述了。而是获取这个枚举域的PropertiesDesc对象。然后返回它的Desc属性。
当然,你还可以把上面的代码改成通用的,把Properties改成一个Type,这样就可以处理所有的枚举。然后还可以在查找PropertiesDesc的位置增加一个缓存。根据Type和字段的Name做缓存。改完后代码如下:

[C#] 纯文本查看 复制代码

public class PropertiesUtils
{
    private  static Dictionary<Type, Dictionary<string, string>> cache = new Dictionary<Type, Dictionary<string, string>>();

    public static string GetDescByProperties(object p)
    {
        var type = p.GetType();
        if (!cache.ContainsKey(type))
        {
            Cache(type);
        }
        var fieldNameToDesc = cache[type];
        var fieldName = p.ToString();
        return fieldNameToDesc.ContainsKey(fieldName) ? fieldNameToDesc[fieldName] : string.Format("Can not found such desc for field `{0}` in type `{1}`", fieldName, type.Name);
    }

    private static void Cache(Type type)
    {
        var dict = new Dictionary<string, string>();
        cache.Add(type, dict);
        var fields = type.GetFields();
        foreach (var field in fields)
        {
            var objs = field.GetCustomAttributes(typeof(PropertiesDesc), true);
            if (objs.Length > 0)
            {
                dict.Add(field.Name, ((PropertiesDesc)objs[0]).Desc);
            }
        }
    }
}

看看Cache方法,里面居然指定了PropertiesDesc!那不是不能通用这个缓存器了!那不行。我们再优化下代码,用泛型来处理这事。

3、泛型优化缓存器
既然是通用的,那我就不叫PropertiesUtils了。改叫AttributeUtil吧。
代码如下:

[C#] 纯文本查看 复制代码

public class AttributeUtils
{
    private  static Dictionary<Type, Dictionary<string, string>> cache = new Dictionary<Type, Dictionary<string, string>>();

    public static string GetDescByProperties<T>(object p)
    {
        var type = p.GetType();
        if (!cache.ContainsKey(type))
        {
            Cache<T>(type);
        }
        var fieldNameToDesc = cache[type];
        var fieldName = p.ToString();
        return fieldNameToDesc.ContainsKey(fieldName) ? fieldNameToDesc[fieldName] : string.Format("Can not found such desc for field `{0}` in type `{1}`", fieldName, type.Name);
    }

    private static void Cache<T>(Type type)
    {
        var dict = new Dictionary<string, string>();
        cache.Add(type, dict);
        var fields = type.GetFields();
        foreach (var field in fields)
        {
            var objs = field.GetCustomAttributes(typeof(T), true);
            if (objs.Length > 0)
            {
                T obj = (T)objs[0];
                Type attrType = obj.GetType();
                PropertyInfo pi = attrType.GetProperty("Desc");
                UnityEngine.Debug.Log(pi);

                dict.Add(field.Name, pi.GetValue(obj, null).ToString());
            }
        }
    }
}

使用这个通用工具来获取描述的时候要注意,枚举上面可以定义不同的Attribute来获取描述信息,但描述信息必须保存在一个叫Desc的Property里面。如果你连这个名字也想自定义,那也可以增加一个参数来获取Property的名字即可。这个留给有需要的读者自己实现了。增加这个特性都不用增加代码行数的-_-。
修改后只要这样调用就可以了。

[C#] 纯文本查看 复制代码

        Debug.Log(PropertiesUtils.GetDescByProperties<PropertiesDesc>(Properties.HP));
        Debug.Log(PropertiesUtils.GetDescByProperties<PropertiesDesc>(Properties.PhyAtk));


4、还能干什么?
    Attribute能干的事情太多了,比如你写了个类,想做ORM映射,里面有些字段不想映射到表,有些想映射到表。有些字段可能名字和表的字段不一样。这时候你就可以通过Attribute来标识哪个字段需要被映射,映射到数据库哪个字段。等等。
    做过网络框架的同学也应该比较熟悉的一个应用,使用Attribute来做自动的消息派发。
    总之,Attribute可以做很多自动化的事情,就看你怎么用了。