将枚举映射到“子枚举”

6
我有一个包含“产品”的数据库。 这些产品被分类,有一个“类别”和一个“子类别”。
例如:
Product p1 = new Product()
{ 
    Category = Category.Fruit, 
    Subcategory = Subcategory.Apple 
 };

我的问题是我想根据类别限制子类别。
以下示例不应该被允许:
Product p2 = new Product()
{ 
    Category = Category.Fruit, 
    Subcategory = Subcategory.Cheese 
};

此外,我希望能够返回一个字符串数组(与每个类别枚举相匹配),其中每个字符串都具有相应子类别的数组。
我思考了一段时间,但没有想出任何解决方案,也没有在网上找到任何解决方案。
有什么建议吗?

1
除非您为不同的子类别组使用单独的枚举,否则无法在编译时完成此操作。有许多不同的方法可以在运行时进行验证。 - D Stanley
7
一个继承层次结构可能更适合您的需求。 - Sam I am says Reinstate Monica
这是业务逻辑,而不是纯技术解决方案。基本上,类别与子类别必须在某个地方(数据库/代码)定义,并进行查找。 - Consult Yarla
每个类别都实现了一个抽象类Product。 - paparazzo
这不是很规范的方法,但如果你很着急,你可以将Category和SubCategory拼接成一个枚举: enum Cats{ Fruit_Apple, Fruit_Pear } - Philip Pittle
4个回答

12
我喜欢地图规则。 你也可以在枚举值上放置自定义属性。
例如:
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

非常感谢 - 这就是我最终使用的。效果很好! - JensOlsen112

3
你所尝试的是使用enum来表示一阶逻辑(http://en.wikipedia.org/wiki/First-order_logic),并将其与数据库同步。如果在代码中硬编码,这不是一项易于完成的任务。许多好的解决方案已经被提出。
个人而言,我会仅使用字符串(或唯一的 ID)来表示类别和子类别,并使用数据库中定义的规则强制执行完整性。但是,如果你最终要在代码中使用它,它将不能在编译时检查。
枚举的问题在于它必须与你的外部资源和代码匹配。此外,要附加更多信息,如价格、国家或即使您有不同种类的苹果,都变得困难。

2

我的建议是创建一个 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]; }
    }
}

0

我成功修改了@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);
    }

}

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接