转换方法以使用任何枚举

Converting a method to use any Enum

我的问题:

我想将我的 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];

    }

到目前为止我尝试过的:

我尝试过使用泛型。他们大部分都成功了,但我无法将我的枚举元素转换为 int 值。我在下面包含了一段给我带来问题的代码示例。

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

我也尝试过使用 Enum e 作为方法参数。我无法使用此方法声明我的枚举元素数组的类型。

假设您的枚举值都是 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);

人们似乎对输入枚举中的多个无法区分的枚举值感到困惑(我仍然认为上面的代码提供了预期的行为)。请注意,这里没有 no 答案,即使是 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)];
}

(注意:如果此代码对性能至关重要,您可能需要对其进行调整并为反射数据添加缓存)。

有些你可以做到,有些则不那么容易。我相信以下扩展方法可以完成您所描述的。

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),问题就是随机 class。您可以调整代码以使用 Random 生成的双精度数,这将覆盖 uint,但 Random class 只是没有覆盖 long 和 ulong 的范围。当然,如果这很重要,您可以 use/find/write 一个不同的随机 class。

(注意: 对于冗长的回答,我提前表示歉意。我实际提出的解决方案并没有那么长,但到目前为止,所提出的解决方案存在许多问题我想尝试彻底解决这些问题,为我自己提出的解决方案提供背景)。


在我看来,虽然您实际上已经接受了一个答案并且可能想使用其中任何一个,但到目前为止提供的答案都不正确或有用。

评论者 Ben Voigt 已经指出了您的规范的两个主要缺陷,这两个缺陷都与您在值本身中编码枚举值的权重有关:

  1. 您将枚举的基础类型绑定到随后必须解释该类型的代码。
  2. 两个具有相同权重的枚举值彼此无法区分

这两个问题都可以解决。事实上,虽然您接受的答案(为什么?)未能解决第一个问题,但 Dweeberly 提供的答案确实通过使用 Convert.ToInt32()(可以从 long 转换为 int 就好了,只要数值足够小即可)。

但第二个问题更难解决。 Asad 的回答试图通过从枚举名称开始并将它们解析为它们的值来解决这个问题。这确实导致最终数组被索引,分别包含每个名称的相应条目。但是实际上 using 枚举的代码无法区分两者;就好像这两个名称是一个枚举值,而这个枚举值的概率权重是两个不同名称所用值的总和。

即在您的示例中,枚举条目例如BNegABNeg 将被分别 selected,随机接收这些 selected 值的代码无法知道它是 BNeg 还是 ABNeg 那是 selected。据它所知,这些只是相同值的两个不同名称。

现在,甚至可以解决这个问题(但不是以 Asad 尝试的方式……他的答案仍然是错误的)。例如,如果您要对值中的概率进行编码,同时仍然确保每个名称的值都是唯一的,那么您可以在执行随机 selection 时对这些概率进行解码,这样就可以了。例如:

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,
};

以这种方式声明您的枚举值后,您可以在 selection 代码中将枚举值除以 100 以获得其概率权重,然后可以在各种示例中使用它。同时,每个枚举名称都有一个唯一的值。

但即使解决了 那个 问题,您仍然会遇到与编码选择和概率表示相关的问题。例如,在上面的枚举中,您不能拥有超过 100 个值的枚举,也不能拥有一个权重大于 (2^31 - 1) / 100 的枚举;如果您想要一个包含超过 100 个值的枚举,则需要更大的乘数,但这会进一步限制您的权重值。

在很多情况下(可能是您关心的所有情况),这都不是问题。这些数字足够小,它们都适合。但在您想要尽可能通用的解决方案的情况下,这似乎是一个严重的限制。

这还不是全部。即使编码保持在合理的范围内,您还有另一个重要的限制需要处理:随机 selection 过程需要一个足够大的数组,以包含每个枚举值与其权重一样多的实例。同样,如果值很小,也许这不是一个大问题。但它确实严重限制了您的实现的泛化能力。


那么,怎么办?

我理解试图让每个枚举类型独立的诱惑;这样做有一些明显的好处。但也有一些由此产生的严重缺点,如果您真的尝试以通用方式使用它,那么对迄今为止提出的解决方案的更改将以恕我直言否定大部分(如果不是全部的话)的方式将您的代码结合在一起保持枚举类型独立的优势(主要是:如果你发现你需要修改实现以适应一些新的枚举类型,你将不得不返回并编辑你正在使用的所有其他枚举类型......即当每个类型看起来是独立的,实际上它们都是紧密耦合的。

在我看来,更好的方法是放弃枚举类型本身将对概率权重进行编码的想法。只是接受这将以某种方式单独声明。

此外,恕我直言,最好避免在您的原始问题中提出并反映在其他两个答案中的内存密集型方法。是的,这对于您在这里处理的小值来说很好。但这是一个不必要的限制,只简化了一小部分逻辑,同时在其他方面使它复杂化和受到限制。

我提出以下解决方案,其中枚举值可以是任何你想要的,枚举的基础类型可以是任何你想要的,算法使用内存只与唯一枚举值的数量成比例,而不是与所有概率权重之和的比例。

在此解决方案中,我还通过缓存用于 select 随机值的不变数据结构来解决可能的性能问题。这对您的情况可能有用,也可能没有用,具体取决于您生成这些随机值的频率。但恕我直言,无论如何这是个好主意;生成这些数据结构的前期成本如此之高,以至于如果这些值完全 select 具有任何规律性,它将开始支配代码的 运行 时间成本。即使今天工作正常,为什么要冒险呢? (同样,特别是 鉴于您似乎想要一个通用的解决方案)。

这是基本的解决方案:

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>();

请注意,实际 select 随机值的工作只是选择一个小于权重总和的非负随机整数,然后使用二分搜索找到合适的枚举值。

每个枚举类型一次,代码将构建 table 值和权重总和,用于二进制搜索。此结果存储在缓存字典中,_typeToAggregatedWeights.

还有必须声明的对象,这些对象将在 运行 时用于构建此 table。请注意,_typeToWeightMap 只是为了支持使此方法 100% 通用。如果你想为你想要支持的每个特定类型编写一个不同的命名方法,它仍然可以使用一个通用方法来实现初始化和 selection,但是命名方法会知道正确的对象(例如 _bloodTypeToWeight) 用于初始化。

或者,另一种在保持方法 100% 通用性的同时避免 _typeToWeightMap 的方法是让 _typeToAggregatedWeights 的类型为 Dictionary<Type, Lazy<object>>,并具有字典(Lazy<object> 对象)显式引用该类型的适当权重数组。

换句话说,这个主题有很多变体都可以正常工作。但它们都将具有与上述基本相同的结构;语义相同,性能差异可以忽略不计。

您会注意到的一件事是二分搜索需要自定义 IComparer<T> 实现。那是在这里:

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() 方法正确比较数组元素,允许单个数组包含枚举值及其聚合权重,但将二进制搜索比较限制为仅权重。