将一个方法转换为使用任何枚举类型

3

我的问题:

我想将我的randomBloodType()方法转换为一个静态方法,可以处理任何枚举类型。我希望我的方法可以处理任何类型的枚举,无论是BloodType、DaysOfTheWeek等,并执行如下操作。

一些关于该方法的背景信息:

该方法当前基于分配给每个元素的值从BloodType枚举中选择一个随机元素。具有更高值的元素有更高的被选中概率。

代码:

    public enum BloodType
    {
        // BloodType = Probability
        ONeg = 4,
        OPos = 36,
        ANeg = 3,
        APos = 28,
        BNeg = 1,
        BPos = 20,
        ABNeg = 1,
        ABPos = 5
    };

    public BloodType randomBloodType()
    {
        // Get the values of the BloodType enum and store it in a array
        BloodType[] bloodTypeValues = (BloodType[])Enum.GetValues(typeof(BloodType));
        List<BloodType> bloodTypeList = new List<BloodType>();

        // Create a list where each element occurs the approximate number of 
        // times defined as its value(probability)
        foreach (BloodType val in bloodTypeValues)
        {
            for(int i = 0; i < (int)val; i++)
            {
                bloodTypeList.Add(val);
            }
        }

        // Sum the values
        int sum = 0;
        foreach (BloodType val in bloodTypeValues)
        {
            sum += (int)val;
        }

        //Get Random value
        Random rand = new Random();
        int randomValue = rand.Next(sum);

        return bloodTypeList[randomValue];

    }

我尝试过的方法:

我尝试使用泛型。它们在大部分情况下都起作用了,但是我无法将枚举元素强制转换为整数值。下面是一段给我带来问题的代码示例。

    foreach (T val in bloodTypeValues)
    {
        sum += (int)val; // This line is the problem.
    }

我还尝试了使用枚举类型作为方法参数。但是,我无法使用这种方法声明我的枚举元素数组的类型。


返回(T)(object)i.Value; // 这可能会得到数字值,我不确定 - Jonathan Kittell
2
我无法帮助你,最好使用字典。 - Asad Saeeduddin
可能是[创建将T限制为枚举的通用方法]的重复问题(https://dev59.com/53VD5IYBdhLWcg3wHn2d)。 - krillgar
3
您不能以这种方式使用枚举。因为您已经为ABNegBNeg分配了相同的数字值,所以无法区分它们。 - Ben Voigt
1
请注意:您已接受的答案无法实现您明显想要做的事情。请参阅Ben Voigt上面的评论;当您为两个不同的枚举名称使用相同的权重时,这些枚举名称表示相同的值。您不能独立选择它们。坦白地说,虽然我很希望您能以合理的方式标记/投票答案,但我更关心的是您至少不会使用错误的代码。请确认您理解为什么您提出的设计无法工作,并且您已经采取了不同的方法。 - Peter Duniho
显示剩余6条评论
3个回答

2
(注意:对于冗长的答案,我提前道歉。我的实际解决方案并不是很长,但目前提供的解决方案存在许多问题,我想尽可能详细地解决这些问题,以便为我自己提出的解决方案提供背景)。
在我看来,虽然您已经接受了一个答案,并且可能会考虑使用其中一个,但到目前为止提供的答案都不正确或有用。
评论者Ben Voigt已经指出了您所述规格的两个主要缺陷,这两个缺陷都与您在值本身中编码枚举值的权重有关:
1. 您将枚举的基础类型与必须解释该类型的代码绑定在一起。 2. 具有相同权重的两个枚举值无法区分。
这两个问题都可以解决。事实上,虽然您接受的答案未能解决第一个问题,但Dweeberly提供的答案通过使用Convert.ToInt32()(只要值足够小,就可以将long转换为int)解决了这个问题。
但第二个问题要难得多。Asad的答案试图通过从枚举名称开始解析它们的值来解决这个问题。这确实导致最终数组被索引,其中包含每个名称的相应条目。但是,实际使用枚举的代码无法区分这两个值;实际上,这就像这两个名称是单个枚举值一样,而该单个枚举值的概率权重是用于两个不同名称的值的总和。
也就是说,在您的示例中,虽然将为例如BNeg和ABNeg的枚举条目分别选择,但接收这些随机选择值的代码无法知道选择的是BNeg还是ABNeg。就它所知,这些只是相同值的两个不同名称。
现在,即使可以解决这个问题(但不是Asad尝试的方式……他的答案仍然有问题)。例如,如果您在值中编码概率,同时确保每个名称都具有唯一值,则可以在进行随机选择时解码这些概率,这将起作用。例如:
enum BloodType
{
    // BloodType = Probability
    ONeg = 4 * 100 + 0,
    OPos = 36 * 100 + 1,
    ANeg = 3 * 100 + 2,
    APos = 28 * 100 + 3,
    BNeg = 1 * 100 + 4,
    BPos = 20 * 100 + 5,
    ABNeg = 1 * 100 + 6,
    ABPos = 5 * 100 + 7,
};

声明枚举值后,您可以在选择代码中将枚举值除以100以获取其概率权重,然后可以按照各种示例中所示使用它。同时,每个枚举名称都有一个唯一的值。
但是,即使解决了该问题,您仍然会遇到与编码和表示概率相关的问题。例如,在上面的示例中,您不能拥有超过100个值的枚举,也不能拥有大于(2 ^ 31-1)/ 100的权重;如果您想要具有超过100个值的枚举,则需要更大的乘数,但这将进一步限制权重值。
在许多情况下(也许是所有您关心的情况),这不会成为问题。数字足够小,它们都适合。但是,在看起来需要尽可能通用的解决方案的情况下,这似乎是一个严重的限制。
而且还有。即使编码保持在合理范围内,您还需要处理另一个重要的限制:随机选择过程需要一个足够大的数组,以便为每个枚举值包含与其权重相同数量的实例。同样,如果值很小,也许这不是一个大问题。但是,它确实严重限制了您的实现通用性。
那么,该怎么办呢?
我理解试图使每个枚举类型自包含的诱惑;这样做有一些明显的优点。但是,这也会带来一些严重的缺点,如果您真的尝试以通用方式使用它,则到目前为止提出的解决方案的更改将以IMHO的方式将您的代码紧密地联系在一起,抵消了保持枚举类型自包含的大部分甚至全部优点(主要是:如果发现需要修改实现以容纳某个新的枚举类型,则必须返回并编辑您正在使用的所有其他枚举类型…即使每种类型看起来都是自包含的,但实际上它们都与彼此紧密耦合)。
在我看来,一个更好的方法是放弃枚举类型本身将编码概率权重的想法。只是接受这将以某种方式单独声明。
此外,在我的意见中,最好避免原始问题中提出的并在其他两个答案中反映的占用内存的方法。是的,这对于您在此处处理的小值很好。但是,这是一个不必要的限制,仅使逻辑的一小部分更简单,同时以其他方式使其复杂化和限制化。
我提出以下解决方案,其中枚举值可以是任何您想要的,枚举的基础类型可以是任何您想要的,并且算法仅按照唯一枚举值的数量成比例地使用内存,而不是按照所有概率权重的总和成比例地使用内存。
在这个解决方案中,我还解决了可能存在的性能问题,通过缓存用于选择随机值的不变数据结构。这可能对你的情况有用,也可能没有用,这取决于你生成这些随机值的频率如何。但是,无论如何,我认为这是一个好主意;生成这些数据结构的前期成本非常高,如果这些值经常被选中,它将开始主导代码的运行时成本。即使今天它能正常工作,为什么要冒险呢?(再次强调,尤其是考虑到您似乎想要一个通用的解决方案)。
以下是基本解决方案:
static T NextRandomEnumValue<T>()
{
    KeyValuePair<T, int>[] aggregatedWeights = GetWeightsForEnum<T>();
    int weightedValue =
            _random.Next(aggregatedWeights[aggregatedWeights.Length - 1].Value),

        index = Array.BinarySearch(aggregatedWeights,
            new KeyValuePair<T, int>(default(T), weightedValue),
            KvpValueComparer<T, int>.Instance);

    return aggregatedWeights[index < 0 ? ~index : index + 1].Key;
}

static KeyValuePair<T, int>[] GetWeightsForEnum<T>()
{
    object temp;

    if (_typeToAggregatedWeights.TryGetValue(typeof(T), out temp))
    {
        return (KeyValuePair<T, int>[])temp;
    }

    if (!_typeToWeightMap.TryGetValue(typeof(T), out temp))
    {
        throw new ArgumentException("Unsupported enum type");
    }

    KeyValuePair<T, int>[] weightMap = (KeyValuePair<T, int>[])temp;
    KeyValuePair<T, int>[] aggregatedWeights =
        new KeyValuePair<T, int>[weightMap.Length];
    int sum = 0;

    for (int i = 0; i < weightMap.Length; i++)
    {
        sum += weightMap[i].Value;
        aggregatedWeights[i] = new KeyValuePair<T,int>(weightMap[i].Key, sum);
    }

    _typeToAggregatedWeights[typeof(T)] = aggregatedWeights;

    return aggregatedWeights;
}

readonly static Random _random = new Random();

// Helper method to reduce verbosity in the enum-to-weight array declarations
static KeyValuePair<T1, T2> CreateKvp<T1, T2>(T1 t1, T2 t2)
{
    return new KeyValuePair<T1, T2>(t1, t2);
}

readonly static KeyValuePair<BloodType, int>[] _bloodTypeToWeight =
{
    CreateKvp(BloodType.ONeg, 4),
    CreateKvp(BloodType.OPos, 36),
    CreateKvp(BloodType.ANeg, 3),
    CreateKvp(BloodType.APos, 28),
    CreateKvp(BloodType.BNeg, 1),
    CreateKvp(BloodType.BPos, 20),
    CreateKvp(BloodType.ABNeg, 1),
    CreateKvp(BloodType.ABPos, 5),
};

readonly static Dictionary<Type, object> _typeToWeightMap =
    new Dictionary<Type, object>()
    {
        { typeof(BloodType), _bloodTypeToWeight },
    };

readonly static Dictionary<Type, object> _typeToAggregatedWeights =
    new Dictionary<Type, object>();

请注意,实际选择随机值的工作只是选择一个小于权重总和的非负随机整数,然后使用二分查找来找到适当的枚举值。
每个枚举类型将构建用于二分查找的值和权重总和表。这个结果存储在缓存字典_typeToAggregatedWeights中。
还有必须声明并在运行时用于构建此表的对象。请注意,_typeToWeightMap只是支持使此方法100%通用的方式之一。如果您想为每个要支持的特定类型编写不同命名的方法,则仍可以使用单个通用方法实现初始化和选择,但命名方法将知道用于初始化的正确对象(例如_bloodTypeToWeight)。
另一种避免_typeToWeightMap但仍保持方法100%通用的方法是将_typeToAggregatedWeights的类型设置为Dictionary >,并且将字典的值(Lazy 对象)明确引用类型的适当权重数组。
换句话说,有许多变化都可以正常工作。但它们的结构基本相同; 语义相同,性能差异可以忽略不计。
您会注意到二分查找需要自定义IComparer 实现。它在这里:
class KvpValueComparer<TKey, TValue> :
    IComparer<KeyValuePair<TKey, TValue>> where TValue : IComparable<TValue>
{
    public readonly static KvpValueComparer<TKey, TValue> Instance =
        new KvpValueComparer<TKey, TValue>();

    private KvpValueComparer() { }

    public int Compare(KeyValuePair<TKey, TValue> x, KeyValuePair<TKey, TValue> y)
    {
        return x.Value.CompareTo(y.Value);
    }
}

这使得Array.BinarySearch()方法能够正确比较数组元素,使单个数组包含枚举值和它们的聚合权重,但将二分搜索比较限制在权重上。

这并没有解决问题,反而解决了一个完全不同的问题。 - Asad Saeeduddin
“这并没有解决问题”--当然解决了。声称没有解决问题是荒谬的。问题已经很明确了:从一组枚举值和相应的权重中,根据这些权重随机选择一个值。即使OP提供了错误的实现,也不能以此为借口给他们提供错误的答案,并声称一个实际上有效的解决方案在某种程度上“完全解决了不同的问题”,这样做最好是误解了问题,最坏的情况是无知。 - Peter Duniho
这个问题要求你如何按枚举值本身的权重选择随机枚举值。这个答案没有提供解决这个问题的代码,因此它不能解决这个问题。很简单。 - Asad Saeeduddin
我的意思是,你可以尽情强调“不可能”这个词,但事实仍然是,你可以从枚举中随机选择一个值,并根据该值进行加权。上面的代码已经做到了这一点。当你将其转换回枚举时,字符串表示始终是首先声明的标签,这与根据所需分布随机选择值的问题无关。顺便说一句,不用谢 :) - Asad Saeeduddin
2
不,你不行。如果你的意思是“文字值”而非枚举名称本身,这个做法最多只能让一个值被随机选中的概率与该值在枚举中出现的次数成正比。很明显,在楼主提供的情景下,他希望将不同的枚举名称视为不同的值,但如果它们所对应的值相同,那么这是根本不可能实现的(再一次强调)。 - Peter Duniho
显示剩余6条评论

0
假设你的枚举值都是int类型(如果它们是longshort或其他类型,可以相应地进行调整):
static TEnum RandomEnumValue<TEnum>(Random rng)
{
    var vals = Enum
        .GetNames(typeof(TEnum))
        .Aggregate(Enumerable.Empty<TEnum>(), (agg, curr) =>
        {
            var value = Enum.Parse(typeof (TEnum), curr);
            return agg.Concat(Enumerable.Repeat((TEnum)value,(int)value)); // For int enums
        })
        .ToArray();

    return vals[rng.Next(vals.Length)];
}

以下是如何使用它的方法:

var rng = new Random();
var randomBloodType = RandomEnumValue<BloodType>(rng);

人们似乎对输入枚举中多个不可区分的枚举值感到困惑(我仍然认为上面的代码提供了预期的行为)。请注意,这里没有任何答案,甚至包括Peter Duniho的答案,都无法让您区分具有相同值的枚举条目,因此我不确定为什么这被视为任何潜在解决方案的度量标准。

尽管如此,一种不使用枚举值作为概率的替代方法是使用属性来指定概率:

public enum BloodType
{
    [P=4]
    ONeg,
    [P=36]
    OPos,
    [P=3]
    ANeg,
    [P=28]
    APos,
    [P=1]
    BNeg,
    [P=20]
    BPos,
    [P=1]
    ABNeg,
    [P=5]
    ABPos
}

这是上面使用的属性的样子:

[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public class PAttribute : Attribute
{
    public int Weight { get; private set; }

    public PAttribute(int weight)
    {
        Weight = weight;
    }
}

最后,这就是获取随机枚举值的方法:

static TEnum RandomEnumValue<TEnum>(Random rng)
{
    var vals = Enum
        .GetNames(typeof(TEnum))
        .Aggregate(Enumerable.Empty<TEnum>(), (agg, curr) =>
        {
            var value = Enum.Parse(typeof(TEnum), curr);

            FieldInfo fi = typeof (TEnum).GetField(curr);
            var weight = ((PAttribute)fi.GetCustomAttribute(typeof(PAttribute), false)).Weight;

            return agg.Concat(Enumerable.Repeat((TEnum)value, weight)); // For int enums
        })
        .ToArray();

    return vals[rng.Next(vals.Length)];
}

(注:如果这段代码对性能要求很高,您可能需要调整它并为反射数据添加缓存。)

@BenVoigt 这很奇怪,在VS 2013上的.NET 4.5中运行良好。 - Asad Saeeduddin
1
哦,我明白了,你说它需要根据枚举的基本类型进行更改。这有点违背泛型的初衷。 - Ben Voigt
@BenVoigt 我使用泛型不是为了任何编译时安全性的好处(反射在这里是不可避免的),而是因为我喜欢它比传递 typeof 任何枚举更好的语法。你的情况可能有所不同。 - Asad Saeeduddin
@Asad 谢谢你提供这个绝妙的解决方案。由于这是我第一次真正接触C#中的lambda函数、可枚举和Aggregate函数,所以我花了一些时间才理解它们的作用。你的解决方案非常棒,我会花一些时间来学习它。 - StenBone
2
但这是病态输入的问题;即对于同一值有两个名称。两个不同的枚举名称具有相同的权重并不是什么“病态”问题。这是一个完全合理和可行的输入;只有因为OP和随后的你选择了一个错误的实现,所以它才是病态的。你的答案没有解决OP实际需要的问题,而是继续使用他们错误的方法,这毫无用处。也就是说,在OP的情况下,无法成功使用你的答案。 - Peter Duniho
显示剩余9条评论

-1

有些你能做到,有些却不那么容易。我相信下面的扩展方法会做你所描述的事情。

static public class Util {
    static Random rnd = new Random();
    static public int PriorityPickEnum(this Enum e) {
        // The approved types for an enum are byte, sbyte, short, ushort, int, uint, long, or ulong
        // However, Random only supports a int (or double) as a max value.  Either way
        // it doesn't have the range for uint, long and ulong.
        //
        // sum enum 
        int sum = 0;
        foreach (var x in Enum.GetValues(e.GetType())) {
            sum += Convert.ToInt32(x);
            }

        var i = rnd.Next(sum); // get a random value, it will form a ratio i / sum

        // enums may not have a uniform (incremented) value range (think about flags)
        // therefore we have to step through to get to the range we want,
        // this is due to the requirement that return value have a probability
        // proportional to it's value.  Note enum values must be sorted for this to work.
        foreach (var x in Enum.GetValues(e.GetType()).OfType<Enum>().OrderBy(a => a)) {
            i -= Convert.ToInt32(x);
            if (i <= 0) return Convert.ToInt32(x);
            }
        throw new Exception("This doesn't seem right");
        }
    }

这是使用此扩展的示例:

        BloodType bt = BloodType.ABNeg;
        for (int i = 0; i < 100; i++) {
            var v = (BloodType) bt.PriorityPickEnum();
            Console.WriteLine("{0}:  {1}({2})", i, v, (int) v);
            }

对于 byte、sbyte、ushort、short 和 int 类型的枚举,这应该可以很好地工作。一旦超出了 int(uint、long、ulong),问题就在于 Random 类。您可以调整代码以使用由 Random 生成的双精度浮点数,这将涵盖 uint,但是 Random 类的范围不足以涵盖 long 和 ulong。当然,如果这很重要,您可以使用/查找/编写不同的 Random 类。


1
在运行时,如果枚举的基础类型不是“int”或“uint”,则会出现无效转换异常--http://rextester.com/GGGJQ16552 - Ben Voigt
我明白,真烦人。我已经更新了代码以解决这个问题。你可以将Convert.ToInt32更改为Convert.ToInt64并获取longs,但仍然会遇到范围问题。 - Dweeberly
我并没有非常仔细地调试这个问题,但它肯定是无效的。你和 OP 的代码以及 Asad 的建议存在相同的根本问题,即已声明的枚举类型无法区分 BNegABNeg。演示代码也是错误的,因为 v 的编译时类型是 int,所以输出不显示枚举值名称。你应该在某个地方进行 BloodType 的转换。 - Peter Duniho
@PeterDuniho,我认为问题/问题的核心并不是BNeg和ABNeg,Asad在您上面的回复加强了这一点。 这是一个有效的观点,但根据文档:“如果多个枚举成员具有相同的基础值,...,您的应用程序代码永远不应依赖于该方法返回特定成员的名称。” https://msdn.microsoft.com/zh-cn/library/system.enum.getname.aspx。 至于将'v'转换为BloodType,我认为您没有查看代码。 至于它不起作用,我认为您没有运行代码。 - Dweeberly
我的观点(与其他评论一样)是,OP的原始提议根本不起作用,也无法起作用。提供解决基本语法问题的代码并不能解决更根本的问题,即过载枚举值以表示权重的想法是错误的。至于强制转换,你是对的...我在测试时忽略了我已经微调了你的代码,并在此过程中失去了强制转换。对于造成的困惑,我深表歉意。 - Peter Duniho
我同意你的评估,但不同意你的结论。我相信我可以修改我提供的代码,创建一个字典<int,List<string>>,将枚举值映射到枚举名称列表。选择一个值后,我可以随机选择列表中的一个名称。然而,文档表明可能已经发生了这种情况。无论如何,我认为这种额外的复杂性并没有改善答案。如果你认为这对其他人有帮助,我很乐意将这样的代码添加到我的答案中;也许还可以简短地讨论一下可能的负面性能影响。 - Dweeberly

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