(注意:对于冗长的答案,我提前道歉。我的实际解决方案并不是很长,但目前提供的解决方案存在许多问题,我想尽可能详细地解决这些问题,以便为我自己提出的解决方案提供背景)。
在我看来,虽然您已经接受了一个答案,并且可能会考虑使用其中一个,但到目前为止提供的答案都不正确或有用。
评论者Ben Voigt已经指出了您所述规格的两个主要缺陷,这两个缺陷都与您在值本身中编码枚举值的权重有关:
1. 您将枚举的基础类型与必须解释该类型的代码绑定在一起。
2. 具有相同权重的两个枚举值无法区分。
这两个问题都可以解决。事实上,虽然您接受的答案未能解决第一个问题,但Dweeberly提供的答案通过使用Convert.ToInt32()(只要值足够小,就可以将long转换为int)解决了这个问题。
但第二个问题要难得多。Asad的答案试图通过从枚举名称开始解析它们的值来解决这个问题。这确实导致最终数组被索引,其中包含每个名称的相应条目。但是,实际使用枚举的代码无法区分这两个值;实际上,这就像这两个名称是单个枚举值一样,而该单个枚举值的概率权重是用于两个不同名称的值的总和。
也就是说,在您的示例中,虽然将为例如BNeg和ABNeg的枚举条目分别选择,但接收这些随机选择值的代码无法知道选择的是BNeg还是ABNeg。就它所知,这些只是相同值的两个不同名称。
现在,即使可以解决这个问题(但不是Asad尝试的方式……他的答案仍然有问题)。例如,如果您在值中编码概率,同时确保每个名称都具有唯一值,则可以在进行随机选择时解码这些概率,这将起作用。例如:
enum BloodType
{
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>()
static KeyValuePair<T, int>[] GetWeightsForEnum<T>()
if (!_typeToWeightMap.TryGetValue(typeof(T), out temp))
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++)
_typeToAggregatedWeights[typeof(T)] = aggregatedWeights;
return aggregatedWeights;
}
readonly static Random _random = new Random();
static KeyValuePair<T1, T2> CreateKvp<T1, T2>(T1 t1, T2 t2)
readonly static KeyValuePair<BloodType, int>[] _bloodTypeToWeight =
;
readonly static Dictionary<Type, object> _typeToWeightMap =
new Dictionary<Type, object>()
,
};
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()
方法能够正确比较数组元素,使单个数组包含枚举值和它们的聚合权重,但将二分搜索比较限制在权重上。
ABNeg
和BNeg
分配了相同的数字值,所以无法区分它们。 - Ben Voigt