65.9K
CodeProject 正在变化。 阅读更多。
Home

枚举字段属性的实用工具

starIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIconemptyStarIcon

1.92/5 (4投票s)

2018 年 6 月 25 日

CPOL

4分钟阅读

viewsIcon

4599356

downloadIcon

56

枚举字段通常需要在 UI 上显示或输出到某些持久化存储时映射到键和人类可读的名称

引言

这是一个用 C# 以简单形式定义枚举及其关联数据的想法。将枚举值与相关信息进行映射需要大量的代码,并且会使枚举定义代码变得混乱。本文提供了一种解决方案,通过利用属性的力量,使此类代码更简单、更直观、更具可读性。

问题

当定义一个具有有限数量有效值的属性时,我们会使用枚举。从本质上讲,这使得代码中的这些值具有可读性,但不足以作为键名,也足以向用户显示。例如,当存在以下键值及其显示标题时:

标题
PS-4 索尼 PlayStation 4
XBOX-ONE 微软 Xbox One
SWITCH 任天堂 Switch

键不是数字值,因此它们不能是枚举值,并且有些键名不能是枚举值名称,因为它们包含连字符。因此,我们需要有方法将enum值转换为键,并将键转换回enum值。

当将它们显示给用户作为属性的选项时,显示enum值名称对用户体验来说并不好。我们还需要为每个enum值提供一个标题。

以下测试代码模拟了用户首先收到一个选项列表并选择其中一个,然后系统从所选选项的键中获取一个枚举值。

    [TestFixture]
    public class EnumUtilityTest
    {
        [Test]
        public void Test()
        {
            var options = GameConsoleUtility.GetAll();

            var optionDisplay = options
                .ToDictionary(o => o.ToKey(), o => $"{o.ToCaption()} [{o.ToKey()}]");

            var selectedKey = "XBOX-ONE";
            var enumValue = selectedKey.ToGameConsole();

            Assert.That(enumValue, Is.EqualTo(GameConsole.XboxOne));
        }
    }

背景

在处理此问题时,我希望在解决方案中实现以下几点,以提高代码的可读性,并相应地提高可维护性:

  • 为每个关联数据命名

这不仅能阐明枚举声明代码中每个关联数据的目的,还能轻松地在您数百万行的代码中查找特定关联数据的用法/引用。

  • 尽可能通用化逻辑

正如我希望为关联数据命名一样,每个枚举声明都需要自定义代码。为了简化每个代码,共享通用代码至关重要。

在我们深入研究我的解决方案之前,让我们先看看其他两种尝试。它们也解决了问题,但我并不喜欢它们,这就是我写这篇技巧的原因。它们可能过于无意义,但请仅将它们视为与后续解决方案的对比。

一种尝试

以下实现可以通过使用 3 组一对一映射来满足需求。但我不喜欢它。

在这种实现中,如果我们想添加一个新的枚举值,我们需要确保在其他两个部分添加一行,如果我们与许多开发人员在一个团队中工作,这可能会出错。此外,它的可读性不高,因为每个枚举值的映射定义是分开的。

    public enum GameConsole
    {
        PS4,
        XboxOne,
        Switch
    }

    public static class GameConsoleUtility
    {
        static Dictionary<gameconsole, string=""> 
        _keyMapping = new Dictionary<gameconsole, string="">
        {
            { GameConsole.PS4, "PS-4" },
            { GameConsole.XboxOne, "XBOX-ONE" },
            { GameConsole.Switch, "SWITCH" },
        };

        static Dictionary<string, gameconsole=""> _antiKeyMapping;

        static Dictionary<gameconsole, string=""> 
        _captionMapping = new Dictionary<gameconsole, string="">
        {
            { GameConsole.PS4, "Sony PlayStation 4" },
            { GameConsole.XboxOne, "Microsoft Xbox One" },
            { GameConsole.Switch, "Nintendo Switch" },
        };

        public static string ToKey(this GameConsole enumValue)
        {
            string key;
            if (!_keyMapping.TryGetValue(enumValue, out key))
                throw new Exception($"No mapping is specified for {enumValue.ToString()}");
            return key;
        }

        public static GameConsole ToGameConsole(this string key)
        {
            if (_antiKeyMapping == null)
            {
                _antiKeyMapping = new Dictionary<string, gameconsole="">();
                foreach (var pair in _keyMapping)
                    _antiKeyMapping.Add(pair.Value, pair.Key);
            }

            GameConsole enumValue;
            if (!_antiKeyMapping.TryGetValue(key, out enumValue))
                throw new Exception($"Invalid key for GameConsole: {key}");
            return enumValue;
        }

        public static string ToCaption(this GameConsole enumValue)
        {
            string caption;
            if (!_captionMapping.TryGetValue(enumValue, out caption))
                throw new Exception($"No mapping is specified for {enumValue.ToString()}");
            return _captionMapping[enumValue];
        }

        public static List<gameconsole> GetAll()
        {
            return _keyMapping.Keys.ToList();
        }
    }

另一种尝试

下面的实现将单独的映射合并为一组映射。我认为这更好,因为代码更简单,数据命名更清晰。但是,我仍然不喜欢它。

尽管如此,与枚举声明相比,与关联数据的映射还是有点偏离。此外,每个枚举类型都有这么多代码对我来说并不理想。

    public enum GameConsole
    {
        PS4,
        XboxOne,
        Switch
    }

    public static class GameConsoleUtility
    {
        internal class GameConsoleMapping
        {
            public GameConsole EnumValue { get; set; }
            public string Key { get; set; }
            public string Caption { get; set; }
            public GameConsoleMapping(GameConsole gameConsole, string key, string caption)
            {
                EnumValue = gameConsole;
                Key = key;
                Caption = caption;
            }
        }

        static List<gameconsolemapping> _mappings = new List<gameconsolemapping>
        {
            new GameConsoleMapping(GameConsole.PS4, key: "PS-4", caption: "Sony PlayStation 4"),
            new GameConsoleMapping(GameConsole.XboxOne, key: "XBOX-ONE", caption: "Microsoft Xbox One"),
            new GameConsoleMapping(GameConsole.Switch, key: "SWITCH", caption: "Nintendo Switch"),
        };

        public static string ToKey(this GameConsole enumValue)
        {
            var key = _mappings.FirstOrDefault(m => m.EnumValue == enumValue)?.Key;
            if (key == null)
                throw new Exception($"No mapping is specified for {enumValue.ToString()}");
            return key;
        }

        public static GameConsole ToGameConsole(this string key)
        {
            var enumValue = _mappings.FirstOrDefault(m => m.Key == key)?.EnumValue;
            if (!enumValue.HasValue)
                throw new Exception($"Invalid key for GameConsole: {key}");
            return enumValue.Value;
        }

        public static string ToCaption(this GameConsole enumValue)
        {
            var caption = _mappings.FirstOrDefault(m => m.EnumValue == enumValue)?.Caption;
            if (caption == null)
                throw new Exception($"No mapping is specified for {enumValue.ToString()}");
            return caption;
        }

        public static List<gameconsole> GetAll()
        {
            return _mappings.Select(m => m.EnumValue).ToList();
        }
    }

因此,我需要一个解决方案,它更加

  • 更具可读性和直观性
  • 每个枚举类型的代码更简单

解决方案

所以,对于解决方案,我需要

  • 此类数据映射代码靠近枚举类型声明
  • 每个枚举的代码更简单

对于我的第一个愿望,我认为使用属性是理想的。我尝试尽可能地通用化代码,并提出了以下解决方案。

让我们先看看我提取的通用代码。

    [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
    public class AliasAttribute : Attribute
    {
        public string[] Aliases { get; }
        public AliasAttribute(params string[] aliases)
        {
            Aliases = aliases;
        }
        public string this[int i]
        {
            get
            {
                if (Aliases.Length > i)
                    return Aliases[i];
                return null;
            }
        }
    }

    public static class AttributeUtility
    {
        class AliasMapping
        {
            public IConvertible EnumValue { get; }
            public AliasAttribute Aliases { get; }
            public AliasMapping(IConvertible enumValue, AliasAttribute aliases)
            {
                EnumValue = enumValue;
                Aliases = aliases;
            }
        }

        static Dictionary<Type, IEnumerable<AliasMapping>> 
        _mappingsMap = new Dictionary<Type, IEnumerable<AliasMapping>>();

        static IEnumerable<T> getCustomAttributes<T, U>(this U enumValue)
            where T : Attribute
            where U : struct, IConvertible
        {
            var fieldName = enumValue.ToString();
            return typeof(U).GetField(fieldName).GetCustomAttributes<T>(true);
        }

        static IEnumerable<AliasMapping> getMappings<T>() where T : struct, IConvertible
        {
            IEnumerable<AliasMapping> mappings;
            if (!_mappingsMap.TryGetValue(typeof(T), out mappings))
            {
                mappings = Enum.GetValues(typeof(T)).Cast<T>().Select
                (e => e.getCustomAttributes<AliasAttribute, T>().Select
                (a => new AliasMapping(e, a)).FirstOrDefault());
                _mappingsMap.Add(typeof(T), mappings);
            }
            return mappings;
        }

        public static string ToAlias<T>(this T enumValue, int index) where T : struct, IConvertible
        {
            var mapping = enumValue.getCustomAttributes<AliasAttribute, T>().FirstOrDefault();
            if (mapping == null)
                throw new Exception($"No mapping is defined for {enumValue.ToString()}");
            return mapping[index];
        }

        public static T ToEnum<T>(this string alias, int index) where T: struct, IConvertible
        {
            var mapping = getMappings<T>().FirstOrDefault(m => m.Aliases[index] == alias);
            if (mapping == null)
                throw new Exception($"Invalid alias for {nameof(T)}: {alias}");
            return (T)mapping.EnumValue;
        }

        public static List<T> GetAll<T>() where T : struct, IConvertible
        {
            return Enum.GetValues(typeof(T)).Cast<T>().ToList();
        }
    }

拥有这样的通用代码,这是枚举定义周围的代码。

    public enum GameConsole
    {
        [KeyCaption(key: "PS-4", caption: "Sony PlayStation 4")]
        PS4,
        [KeyCaption(key: "XBOX-ONE", caption: "Microsoft Xbox One")]
        XboxOne,
        [KeyCaption(key: "SWITCH", caption: "Nintendo Switch")]
        Switch
    }

    [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
    public class KeyCaptionAttribute : AliasAttribute
    {
        public KeyCaptionAttribute(string key, string caption) : base(key, caption)
        {
        }
    }

    public static class GameConsoleUtility
    {
        public static string ToKey(this GameConsole enumValue)
        {
            return enumValue.ToAlias<GameConsole>(0);
        }

        public static GameConsole ToGameConsole(this string key)
        {
            return key.ToEnum<GameConsole>(0);
        }

        public static string ToCaption(this GameConsole enumValue)
        {
            return enumValue.ToAlias<GameConsole>(1);
        }

        public static List<GameConsole> GetAll()
        {
            return AttributeUtility.GetAll<GameConsole>();
        }
    }

代码量大大减少,因此整洁。与前两种尝试相比,它不仅更具可读性,而且更易于维护代码。

我希望它能帮助您使代码整洁,并提高可读性和可维护性的质量。

备注

对于要在 UI 上显示的标题或类似内容,会涉及到另一个问题,即本地化。我认为这是另一个问题。本文主要关注与枚举相关的关联数据。例如,即使在全局化的程序或系统中,您也可以有一个或多个关联键,如标题键、工具提示消息键、子选项引用。

如果您需要使用此实用工具类的本地化功能,Sacha Barber 的文章可以为您提供思路。

如何使用附加文件

附加文件 EnumFieldAttributeUtilities.zip 包含一个 VS 解决方案。该解决方案具有名为 Attempt1、Attempt2 和 Solution 的生成配置,因此请在运行测试代码之前在 VS 中切换生成配置。

VS 解决方案是在 VS Community 2017 for Mac 上创建的。如果您在使用 Windows 时遇到问题,请留言。

历史

  • 2018/06/26:发布第一版。
  • 2018/06/29:附加了包含本文代码的 VS 解决方案的 zip 文件。还添加了如何使用的部分。
  • 2018/06/29:在备注部分添加了指向Sacha Barber 的文章的链接。
  • 2018/07/12:根据评论中的讨论,进一步阐明了此解决方案的背景。

 

© . All rights reserved.