转换方法以使用任何枚举

时间:2015-04-26 00:31:12

标签: c# generics enums

我的问题:

我想将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作为方法参数。我无法使用此方法声明我的枚举元素数组的类型。

3 个答案:

答案 0 :(得分:2)

注意:我提前为这个冗长的答案道歉。我实际提出的解决方案并不是那么久,但到目前为止提出的解决方案存在许多问题,我想尝试一下彻底解决这些问题,为我自己提出的解决方案提供背景信息。)


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

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

  1. 您正在将枚举的基础类型绑定到必须解释该类型的代码。
  2. 两个具有相同权重的枚举值无法区分
  3. 这两个问题都可以解决。事实上,虽然您接受的答案(为什么?)未能解决第一个问题,但Dweeberly提供的答案确实通过使用Convert.ToInt32()(可以从long转换为int来解决这个问题。很好,只要值足够小。)

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

    即。在你的例子中,虽然例如enum条目将单独选择BNegABNeg,接收这些随机选择的值的代码无法知道是选择了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个值的枚举,你需要一个更大的乘数,但这会更加限制你的权重值。

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

    并非全部。即使编码保持在合理的限制范围内,您还有另一个重要的限制:随机选择过程需要一个足够大的数组,以便为​​每个枚举值包含与其权重一样多的该值的实例。同样,如果值很小,这可能不是一个大问题。但它确实严重限制了你的实现概括的能力。


    那么,该怎么办?

    我理解试图让每个枚举类型保持独立的诱惑;这样做有一些明显的好处。但是也有一些严重的缺点,如果你真的试图以一种普遍的方式使用它,那么到目前为止所提出的解决方案的改变将把你的代码绑定在一起,以及恕我直言的否定最多的方式,如果不是全部的话保持枚举类型自包含的优点(主要是:如果您发现需要修改实现以适应一些新的枚举类型,则必须返回并编辑您正在使用的所有其他枚举类型...即虽然每种类型看起来都是独立的,但实际上它们都是紧密耦合的。)

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

    此外,恕我直言会更好地避免在原始问题中提出的内存密集型方法,并在其他两个答案中进行反映。是的,这对你在这里处理的小值很好。但这是一个不必要的限制,只使逻辑的一小部分更简单,同时以其他方式使其复杂化和限制。

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

    在此解决方案中,我还通过缓存用于选择随机值的不变数据结构来解决可能的性能问题。这种情况在您的情况下可能有用,也可能没用,具体取决于生成这些随机值的频率。但恕我直言,这是一个好主意,不管;生成这些数据结构的前期成本非常高,如果根据任何规律选择值,它将开始主导代码的运行时成本。即使今天工作正常,为什么要承担风险呢? (再次,特别是,因为你似乎想要一个通用的解决方案。)

    以下是基本解决方案:

    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)用于初始化。

    或者,在保持方法100%通用的同时避免_typeToWeightMap的另一种方法是让_typeToAggregatedWeightsDictionary<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()方法更正比较数组元素,允许单个数组包含枚举值及其聚合权重,但将二进制搜索比较限制为权重。

答案 1 :(得分: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)];
}

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

答案 2 :(得分:-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类。