将枚举映射到“子枚举”

本文关键字:枚举 子枚举 映射 | 更新日期: 2023-09-27 18:27:08

我有一个包含产品的数据库。这些产品已分类,具有类别和子类别

例如:

Product p1 = new Product()
{ 
    Category = Category.Fruit, 
    Subcategory = Subcategory.Apple 
 };

我的问题是我想根据类别限制子类别。

以下示例应该是不可能的:

Product p2 = new Product()
{ 
    Category = Category.Fruit, 
    Subcategory = Subcategory.Cheese 
};

此外,我希望能够返回一个字符串数组(匹配每个类别枚举(,每个字符串都有一个相应子类别的数组。

我已经思考了一段时间,但一无所获,也没有在网上找到任何解决方案。

会有什么建议?

将枚举映射到“子枚举”

我喜欢地图规则。您还可以在枚举值上放置自定义属性。

例如:

public enum Subcategory {
    [SubcategoryOf(Category.Fruit)]
    Apple,
    [SubcategoryOf(Category.Dairy)]
    Emmenthaler
}

这要求您编写一个SubcategoryOfAttribute类(有关 MS 指南,请参阅此处(。然后,您可以编写一个验证程序,该验证程序可以查看任何子类别并从中获取合法的父类别。

与地图相比,这样做的好处是关系在声明中得到了很好的阐述。

缺点是每个子类别最多可以有一个父类别。

我发现这很有趣,所以我把它存了下来。 首先是属性:

[AttributeUsage(AttributeTargets.Field)]
public class SubcategoryOf : Attribute {
    public SubcategoryOf(Category cat) {
        Category = cat;
    }
    public Category Category { get; private set; }
}

然后我们做一些模拟枚举

public enum Category {
    Fruit,
    Dairy,
    Vegetable,
    Electronics
}
public enum Subcategory {
    [SubcategoryOf(Category.Fruit)]
    Apple,
    [SubcategoryOf(Category.Dairy)]
    Buttermilk,
    [SubcategoryOf(Category.Dairy)]
    Emmenthaler,
    [SubcategoryOf(Category.Fruit)]
    Orange,
    [SubcategoryOf(Category.Electronics)]
    Mp3Player
}

现在我们需要一个谓词来确定子类别是否与类别匹配(注意:如果需要,您可以有多个父类别 - 您需要修改属性和此谓词以获取所有属性并检查每个属性。

public static class Extensions {
    public static bool IsSubcategoryOf(this Subcategory sub, Category cat) {
        Type t = typeof(Subcategory);
        MemberInfo mi = t.GetMember(sub.ToString()).FirstOrDefault(m => m.GetCustomAttribute(typeof(SubcategoryOf)) != null);
        if (mi == null) throw new ArgumentException("Subcategory " + sub + " has no category.");
        SubcategoryOf subAttr = (SubcategoryOf)mi.GetCustomAttribute(typeof(SubcategoryOf));
        return subAttr.Category == cat;
    }
}

然后,您输入您的产品类型进行测试:

public class Product {
    public Product(Category cat, Subcategory sub) {
        if (!sub.IsSubcategoryOf(cat)) throw new ArgumentException(
            String.Format("{0} is not a sub category of {1}.", sub, cat), "sub");
        Category = cat;
        Subcategory = sub;
    }
    public Category Category { get; private set; }
    public Subcategory Subcategory { get; private set; }
}

测试代码:

Product p = new Product(Category.Electronics, Subcategory.Mp3Player); // succeeds
Product q = new Product(Category.Dairy, Subcategory.Apple); // throws an exception

您要做的是使用枚举表示一阶逻辑(http://en.wikipedia.org/wiki/First-order_logic(。并与数据库同步。在代码中对其进行硬编码时,这不是一件容易的事。已经提出了许多好的解决方案。

就我而言,我只会对类别和子类别使用字符串(或唯一 ID(,并使用数据库中定义的规则强制执行完整性。但是,如果您最终在代码中使用它,则它不会是编译时。

Enum 的问题在于它必须与外部源代码和代码匹配。此外,很难附加更多信息,例如价格或国家,或者即使您有不同种类的苹果。

我的建议是有一个Dictionary<SubCategory, Category>,将您的SubCategory映射到您的Category

之后,您可以一起摆脱产品上的Category,也可以使用辅助方法

public class Product
{
    static Dictionary<SubCategory, Category> _categoriesMap;
    public static Product()
    {
        _categoriesMap = new Dictionary<SubCategory, Category>();
        _categoriesMap.Add(SubCategory.Apple, Category.Fruit);
    }
    public SubCategory SubCategory { get; set; }
    public Category Category
    {
        get { return _categoriesMap[this.SubCategory]; }
    }
}

我能够修改@plinth发布的代码

[AttributeUsage(AttributeTargets.Field,
    AllowMultiple = true)]
public class SubcategoryOf : Attribute
{
    public Enum? Category { get; }
    public SubcategoryOf(object category)
    {
        if (category is Enum categoryEnum) Category = categoryEnum;
    }
}
public static class CategoryExtension
{
    public static bool IsSubcategoryOf(this Enum subcategory,
        Enum category)
    {
        var t = subcategory.GetType();

        /*This section is for returning true if enum is
          of the same category (eg. Product.Fruit is Product.Fruit) */
        if (Equals(subcategory,
                category))
            return true;
        var memberInfo = t.GetMember(subcategory.ToString());

        /*This section loops through all attributes for a match of
           declared category and returns its result true/false */
        foreach (var member in memberInfo)
            if (member.IsDefined(typeof(SubcategoryOf)))
                return member.GetCustomAttributes(typeof(SubcategoryOf))
                    .Cast<SubcategoryOf?>()
                    .Any(subCatOf => Equals(subCatOf!.Category,
                        category));
        /*If a category is not assigned a warning is posted to the 
          debug console that an attempt to compare an enum without a
          category was made and returns false, this does not stop the
          program with a throw exception as this should not break the
          intended use of comparing.*/
        Debug.WriteLine($"the enum {subcategory} does not contain a category and was being compared.",
            "Warning(CategoryExtension.IsSubcategoryOf)");
        return false;
    }
}

以下是使用上述类的示例:

public enum Proficiency
{
    ArmorProficiency,
    WeaponProficiency,
    ToolProficiency,
    LanguageProficiency
}
public enum ArmorProficiency
{
    [IsSubcategoryOf(Proficiency.ArmorProficiency)]
    Light,
    [IsSubcategoryOf(Proficiency.ArmorProficiency)]
    Medium,
    [IsSubcategoryOf(Proficiency.ArmorProficiency)]
    Heavy,
    [IsSubcategoryOf(Proficiency.ArmorProficiency)]
    Shield,
}
public enum LightArmorProficiency
{
    [IsSubcategoryOf(ArmorProficiency.Light)] 
    [IsSubcategoryOf(Proficiency.ArmorProficiency)]
    PaddedArmor,
    [IsSubcategoryOf(ArmorProficiency.Light)] 
    [IsSubcategoryOf(Proficiency.ArmorProficiency)]
    LeatherArmor,
    [IsSubcategoryOf(ArmorProficiency.Light)] 
    [IsSubcategoryOf(Proficiency.ArmorProficiency)]
    StuddedLeatherArmor,
}
public enum HeavyArmorProficiency
{
    [IsSubcategoryOf(ArmorProficiency.Heavy)]
    [IsSubcategoryOf(Proficiency.ArmorProficiency)]
    RingMail,
    [IsSubcategoryOf(ArmorProficiency.Heavy)]
    [IsSubcategoryOf(Proficiency.ArmorProficiency)]
    ChainMail,
    [IsSubcategoryOf(ArmorProficiency.Heavy)] 
    [IsSubcategoryOf(Proficiency.ArmorProficiency)]
    SplintMail,
    [IsSubcategoryOf(ArmorProficiency.Heavy)]
    [IsSubcategoryOf(Proficiency.ArmorProficiency)]
    PlateMail,
}

下面是如何将类别类与枚举一起使用的示例。

public class Proficiencies
{
    private readonly Dictionary<Enum, ProficiencyType> _armorProficiencies;
    private readonly Dictionary<Enum, ProficiencyType> _weaponProficiencies;
    private readonly Dictionary<Enum, ProficiencyType> _toolProficiencies;
    private readonly Dictionary<Enum, LanguageComprehension> _languageProficiencies;
    public IReadOnlyDictionary<Enum, ProficiencyType> ArmorProficiencies =>
        _armorProficiencies;
    public IReadOnlyDictionary<Enum, ProficiencyType> WeaponProficiencies =>
        _weaponProficiencies;
    public IReadOnlyDictionary<Enum, ProficiencyType> ToolProficiencies =>
        _toolProficiencies;
    public IReadOnlyDictionary<Enum, LanguageComprehension> LanguageProficiencies =>
        _languageProficiencies;
    public Proficiencies(bool startsWithCommon,
        LanguageComprehension comprehensionLevel = LanguageComprehension.ReadAndWrite)
    {
        _armorProficiencies = new Dictionary<Enum, ProficiencyType>();
        _weaponProficiencies = new Dictionary<Enum, ProficiencyType>();
        _toolProficiencies = new Dictionary<Enum, ProficiencyType>();
        _languageProficiencies = new Dictionary<Enum, LanguageComprehension>();
        if (startsWithCommon)
            _languageProficiencies.Add(StandardLanguageProficiency.Common,
                comprehensionLevel);
    }
    public void AddNonLanguageProficiency(Enum proficiency,
        ProficiencyType proficiencyType)
    {
        if (proficiency.IsSubcategoryOf(Proficiency.ArmorProficiency))
        {
            if (_armorProficiencies.ContainsKey(proficiency))
                _armorProficiencies[proficiency] = proficiencyType;
            else
                _armorProficiencies.Add(proficiency,
                    proficiencyType);
        }
        else if (proficiency.IsSubcategoryOf(Proficiency.WeaponProficiency))
        {
            if (_weaponProficiencies.ContainsKey(proficiency))
                _weaponProficiencies[proficiency] = proficiencyType;
            else
                _weaponProficiencies.Add(proficiency,
                    proficiencyType);
        }
        else if (proficiency.IsSubcategoryOf(Proficiency.ToolProficiency))
        {
            if (_toolProficiencies.ContainsKey(proficiency))
                _toolProficiencies[proficiency] = proficiencyType;
            else
                _toolProficiencies.Add(proficiency,
                    proficiencyType);
        }
        else
        {
            Debug.WriteLine($"The enum {proficiency} is not a valid proficiency and was being added.",
                "Warning(Proficiencies.AddProficiency)");
        }
    }
    public void RemoveProficiency(Enum proficiency)
    {
        if ( _armorProficiencies.ContainsKey(proficiency) )
            _armorProficiencies.Remove(proficiency);
        else if ( _weaponProficiencies.ContainsKey(proficiency) )
            _weaponProficiencies.Remove(proficiency);
        else if ( _toolProficiencies.ContainsKey(proficiency) )
            _toolProficiencies.Remove(proficiency);
        else if (  _languageProficiencies.ContainsKey(proficiency))
            _languageProficiencies.Remove(proficiency);
    }
    
    public void AddLanguageProficiency(Enum language,
        LanguageComprehension comprehension)
    {
        if (!language.IsSubcategoryOf(Proficiency.LanguageProficiency)) return;
        if (_languageProficiencies.ContainsKey(language))
            _languageProficiencies[language] = comprehension;
        else
            _languageProficiencies.Add(language,
                comprehension);
    }
}

总结:在这种情况下,主类别现在由提供的枚举定义,而不是硬编码,子类别类不关心给出的内容,只要它是对象类型枚举。

扩展类现在仅按属性检查子类别是否属于类别或其他类别,并且不对枚举类型进行硬编码。这反过来又允许使用多个枚举集合,而无需将所有内容强制到单个枚举中,如果子类别不属于,它将返回 false。