问题 使用枚举类保持枚举到对象的映射?


我经常需要在枚举和另一个对象(本例中为字符串)之间进行全局硬编码映射。我想共同定位枚举和映射定义以阐明维护。

如您所见,在此示例中,创建了一个带有一个静态字段的恼人​​类。

public enum EmailTemplates
{
    // Remember to edit the corresponding mapping singleton!
    WelcomeEmail,
    ConfirmEmail
}

public class KnownTemplates
{
    public static Dictionary<EmailTemplates, string> KnownTemplates;
    static KnownTemplates() {
        KnownTemplates.Add(EmailTemplates.WelcomeEmail, "File1.htm");
        KnownTemplates.Add(EmailTemplates.ConfirmEmail, "File2.htm");
    }
}

有时,映射类可以具有更多功能和有意义的名称,并且映射活动甚至可以是私有的。但这只会污染维护/关联问题。

任何人都有一个良好的模式吗?


7291
2018-04-24 23:34


起源



答案:


您可以使用属性来注释枚举,然后使用反射来构建字典。

[AttributeUsage(AttributeTargets.Field)]
sealed class TemplateAttribute : Attribute {

  public TemplateAttribute(String fileName) {
    FileName = fileName;
  }

  public String FileName { get; set; }

}

enum EmailTemplate {

  [Template("File1.htm")]
  WelcomeEmail,

  [Template("File2.htm")]
  ConfirmEmail

}

class KnownTemplates {

  static Dictionary<EmailTemplate, String> knownTemplates;

  static KnownTemplates() {
    knownTemplates = typeof(EmailTemplates)
      .GetFields(BindingFlags.Static | BindingFlags.Public)
      .Where(fieldInfo => Attribute.IsDefined(fieldInfo, typeof(TemplateAttribute)))
      .Select(
        fieldInfo => new {
          Value = (EmailTemplate) fieldInfo.GetValue(null),
          Template = (TemplateAttribute) Attribute
            .GetCustomAttribute(fieldInfo, typeof(TemplateAttribute))
        }
      )
      .ToDictionary(x => x.Value, x => x.Template.FileName);
  }

}

如果您经常这样做,您可以创建一个更通用的泛型函数,它将枚举值与与该枚举值相关联的属性组合在一起:

static IEnumerable<Tuple<TEnum, TAttribute>> GetEnumAttributes<TEnum, TAttribute>()
  where TEnum : struct
  where TAttribute : Attribute {
  return typeof(TEnum)
    .GetFields(BindingFlags.Static | BindingFlags.Public)
    .Where(fieldInfo => Attribute.IsDefined(fieldInfo, typeof(TAttribute)))
    .Select(
      fieldInfo => Tuple.Create(
        (TEnum) fieldInfo.GetValue(null),
        (TAttribute) Attribute.GetCustomAttribute(fieldInfo, typeof(TAttribute))
      )
    );
}

并像这样使用它:

knownTemplates = GetEnumAttributes<EmailTemplate, TemplateAttribute>()
  .ToDictionary(tuple => tuple.Item1, tuple => tuple.Item2.FileName);

为了更有趣,您可以创建一个扩展方法:

static class EmailTemplateExtensions {

  static Dictionary<EmailTemplate, String> templates;

  static EmailTemplateExtensions() {
    templates = GetEnumAttributes<EmailTemplate, TemplateAttribute>()
      .ToDictionary(tuple => tuple.Item1, tuple => tuple.Item2.FileName);
  }

  public static String FileName(this EmailTemplate emailTemplate) {
    String fileName;
    if (templates.TryGetValue(emailTemplate, out fileName))
      return fileName;
    throw new ArgumentException(
      String.Format("No template defined for EmailTemplate.{0}.", emailTemplate)
    );
  }

}

然后打电话 EmailTemplate.ConfirmEmail.FileName() 将返回 File2.htm


6
2018-04-24 23:58



我认为他们希望删除类使用,使用类似的语法 string sFile = EmailTemplate.Confirm.TemplateFile;。 - AMissico
马丁!谢谢!对于我的大多数情况(字符串是合适的),属性是一个很好的解决方案。我知道我有一个复杂对象映射到每个枚举的情况,但这甚至可以覆盖一些标量。 - shannon


答案:


您可以使用属性来注释枚举,然后使用反射来构建字典。

[AttributeUsage(AttributeTargets.Field)]
sealed class TemplateAttribute : Attribute {

  public TemplateAttribute(String fileName) {
    FileName = fileName;
  }

  public String FileName { get; set; }

}

enum EmailTemplate {

  [Template("File1.htm")]
  WelcomeEmail,

  [Template("File2.htm")]
  ConfirmEmail

}

class KnownTemplates {

  static Dictionary<EmailTemplate, String> knownTemplates;

  static KnownTemplates() {
    knownTemplates = typeof(EmailTemplates)
      .GetFields(BindingFlags.Static | BindingFlags.Public)
      .Where(fieldInfo => Attribute.IsDefined(fieldInfo, typeof(TemplateAttribute)))
      .Select(
        fieldInfo => new {
          Value = (EmailTemplate) fieldInfo.GetValue(null),
          Template = (TemplateAttribute) Attribute
            .GetCustomAttribute(fieldInfo, typeof(TemplateAttribute))
        }
      )
      .ToDictionary(x => x.Value, x => x.Template.FileName);
  }

}

如果您经常这样做,您可以创建一个更通用的泛型函数,它将枚举值与与该枚举值相关联的属性组合在一起:

static IEnumerable<Tuple<TEnum, TAttribute>> GetEnumAttributes<TEnum, TAttribute>()
  where TEnum : struct
  where TAttribute : Attribute {
  return typeof(TEnum)
    .GetFields(BindingFlags.Static | BindingFlags.Public)
    .Where(fieldInfo => Attribute.IsDefined(fieldInfo, typeof(TAttribute)))
    .Select(
      fieldInfo => Tuple.Create(
        (TEnum) fieldInfo.GetValue(null),
        (TAttribute) Attribute.GetCustomAttribute(fieldInfo, typeof(TAttribute))
      )
    );
}

并像这样使用它:

knownTemplates = GetEnumAttributes<EmailTemplate, TemplateAttribute>()
  .ToDictionary(tuple => tuple.Item1, tuple => tuple.Item2.FileName);

为了更有趣,您可以创建一个扩展方法:

static class EmailTemplateExtensions {

  static Dictionary<EmailTemplate, String> templates;

  static EmailTemplateExtensions() {
    templates = GetEnumAttributes<EmailTemplate, TemplateAttribute>()
      .ToDictionary(tuple => tuple.Item1, tuple => tuple.Item2.FileName);
  }

  public static String FileName(this EmailTemplate emailTemplate) {
    String fileName;
    if (templates.TryGetValue(emailTemplate, out fileName))
      return fileName;
    throw new ArgumentException(
      String.Format("No template defined for EmailTemplate.{0}.", emailTemplate)
    );
  }

}

然后打电话 EmailTemplate.ConfirmEmail.FileName() 将返回 File2.htm


6
2018-04-24 23:58



我认为他们希望删除类使用,使用类似的语法 string sFile = EmailTemplate.Confirm.TemplateFile;。 - AMissico
马丁!谢谢!对于我的大多数情况(字符串是合适的),属性是一个很好的解决方案。我知道我有一个复杂对象映射到每个枚举的情况,但这甚至可以覆盖一些标量。 - shannon


通常,当您想要为枚举元素添加额外的信息或行为时,这意味着您需要一个完整的类。您可以从(old-)Java借用类型安全的枚举模式并创建如下内容:

sealed class EmailTemplate {
  public static readonly EmailTemplate Welcome = new EmailTemplate("File1.html");
  public static readonly EmailTemplate Confirm = new EmailTemplate("File2.html");

  private EmailTemplate(string location) {
    Location = location;
  }
  public string Location { get; private set; }

  public string Render(Model data) { ... }
}

现在,您可以将任何属性或方法与元素相关联,例如 Location 和 Render 以上。


2
2018-04-26 09:47





这是一种对我来说非常好的方法。

public class BaseErrWarn : Attribute
{
    public string Code { get; set; }
    public string Description { get; set; }

    public BaseErrWarn(string code, string description)
    {
        this.Code = code;
        this.Description = description;
    }
}

public enum ErrorCode
{
    [BaseErrWarn("ClientErrMissingOrEmptyField", "Field was missing or empty.")] ClientErrMissingOrEmptyField,
    [BaseErrWarn("ClientErrInvalidFieldValue", "Not a valid field value.")] ClientErrInvalidFieldValue,
    [BaseErrWarn("ClientErrMissingValue", "No value passed in.")] ClientErrMissingValue
}

现在,您可以使用反射将Enum映射到BaseErrWarn类:

public static BaseErrWarn GetAttribute(Enum enumVal)
{
    return (BaseErrWarn)Attribute.GetCustomAttribute(ForValue(enumVal), typeof(BaseErrWarn));
}

private static MemberInfo ForValue(Enum errorEnum)
{
    return typeof(BaseErrWarn).GetField(Enum.GetName(typeof(BaseErrWarn), errorEnum));
}

下面是一个示例,它使用此映射将Enum映射到对象,然后从中拉出字段:

    public BaseError(Enum errorCode)
    {
        BaseErrWarn baseError = GetAttribute(errorCode);
        this.Code = baseError.Code;
        this.Description = baseError.Description;
    }

    public BaseError(Enum errorCode, string fieldName)
    {
        BaseErrWarn baseError = GetAttribute(errorCode);
        this.Code = baseError.Code;
        this.Description = baseError.Description;
        this.FieldName = fieldName;
    }  

2
2017-12-11 11:35



很好的方法,您可以更精细地控制属性,减少旅行中的反射>映射>人类可读形式。 - Tahir Khalid