Fat*_*tie 13 c# arrays generics unity-game-engine
这是一个非常方便的扩展,适用于array
任何事情:
public static T AnyOne<T>(this T[] ra) where T:class
{
int k = ra.Length;
int r = Random.Range(0,k);
return ra[r];
}
Run Code Online (Sandbox Code Playgroud)
不幸的是,它不适合List<>
任何事情.这是适用于任何人的相同扩展名List<>
public static T AnyOne<T>(this List<T> listy) where T:class
{
int k = listy.Count;
int r = Random.Range(0,k);
return listy[r];
}
Run Code Online (Sandbox Code Playgroud)
事实上,是否有一种方法可以一次性推广覆盖array
s和List<>
s的泛型?或者知道不可能吗?
它发生在我身上,答案是否可以进一步包含Collection
s?或者确实有下面的专家之一已经实现了??
PS,我很抱歉没有明确提到这是在Unity3D环境中."Random.Range"是一个统一到骨骼的功能,"AnyOne"调用读取为任何游戏工程师的100%游戏引擎.这是你为任何游戏项目输入的第一个扩展,并且你经常在游戏代码中使用它("任何爆炸!""任何硬币声音效果!"等等!)
显然,它当然可以在任何c#milieu中使用.
T[]
而List<T>
实际上都实现IList<T>
,它提供了枚举,一个Count属性和索引.
public static T AnyOne<T>(this IList<T> ra)
{
int k = ra.Count;
int r = Random.Range(0,k);
return ra[r];
}
Run Code Online (Sandbox Code Playgroud)
请注意:对于Unity3D环境,具体来说,这是正确的答案.关于这个答案的进一步改进IReadOnlyList<T>
,它在Unity3D中不可用.(关于(巧妙)扩展IEnumerable<T>
甚至覆盖没有计数/可索引性的对象的情况,当然在游戏引擎情况下将是一个独特的概念(例如AnyOneEvenInefficiently
或AnyOneEvenFromUnsafeGroups
).)
实际上,您T[]
和List<T>
案例之间最合适的通用接口是IReadOnlyList<T>
public static T AnyOne<T>(this IReadOnlyList<T> list) where T:class
{
int k = list.Count;
int r = Random.Range(0,k);
return list[r];
}
Run Code Online (Sandbox Code Playgroud)
正如另一个答案中所提到的那样,IList<T>
也有效,但是好的做法要求您从调用者请求该方法所需的最小功能,在本例中是Count
属性和只读索引器.
IEnumerable<T>
也可以工作,但它允许调用者传递非集合迭代器,其中Count
和ElementAt
扩展方法可能非常低效 - 比如Enumerable.Range(0, 1000000)
,数据库查询等.
Unity3D工程师的注意事项:如果你查看IReadOnlyList接口文档的最底层,它可以从.Net 4.5开始使用.在.Net的早期版本中,您必须求助于IList<T>
(从2.0开始提供).Unity在.Net版本上远远落后.2016年,Unity仅使用.Net 2.0.5.所以对于Unity3D,你必须使用IList<T>
.
一些人选择的方式很有意思IEnumerable<T>
,而另一些人则坚持这一点IReadOnlyList<T>
.
现在让我们说实话.IEnumerable<T>
是有用的,非常有用.在大多数情况下,您只想将此方法放在某个库中,并将实用程序函数抛出到您认为的集合中,并完成它.但是,IEnumerable<T>
正确使用有点棘手,我在这里指出......
IEnumerable的
让我们假设OP使用Linq并希望从序列中获取随机元素.基本上他最终得到了来自@Yannick的代码,最终在实用程序辅助函数库中:
public static T AnyOne<T>(this IEnumerable<T> source)
{
int endExclusive = source.Count(); // #1
int randomIndex = Random.Range(0, endExclusive);
return source.ElementAt(randomIndex); // #2
}
Run Code Online (Sandbox Code Playgroud)
现在,这基本上做的是两件事:
IEnumerable<T>
这意味着遍历列表中的所有元素,如果它是f.ex. a List<T>
,它将使用该Count
属性.randomIndex
,抓住并返回它.这里有两件事可能出错.首先,您的IEnumerable可能是一个缓慢的顺序存储,并且这样做Count
会以意想不到的方式破坏应用程序的性能.例如,从设备流式传输可能会让您遇到麻烦.也就是说,你可以很好地争辩说,当这个系列的特征固有的时候会有所期待 - 而且我个人认为这个论点会成立.
其次 - 这可能更重要 - 不能保证你的枚举每次迭代都会返回相同的序列(因此也无法保证你的代码不会崩溃).例如,考虑一下这个无辜的代码片段,它可能对测试有用:
IEnumerable<int> GenerateRandomDataset()
{
Random rnd = new Random();
int count = rnd.Next(10, 100); // randomize number of elements
for (int i=0; i<count; ++i)
{
yield return new rnd.Next(0, 1000000); // randomize result
}
}
Run Code Online (Sandbox Code Playgroud)
第一次迭代(调用Count()
),您可能会生成99个结果.您选择元素98.接下来,您调用ElementAt
,第二次迭代生成12个结果,您的应用程序崩溃.不酷.
修复IEnumerable实现
正如我们所见,实现的问题IEnumerable<T>
是你必须经历2次数据.我们可以通过一次浏览数据来解决这个问题.
这里的'技巧'实际上非常简单:如果我们看过1个元素,我们肯定要考虑返回它.考虑到所有因素,这是我们将返回的元素的50%/ 50%的可能性.如果我们看到第三个元素,我们就会有33%/ 33%/ 33%的可能性.等等.
因此,更好的实现可能是这个:
public static T AnyOne<T>(this IEnumerable<T> source)
{
Random rnd = new Random();
double count = 1;
T result = default(T);
foreach (var element in source)
{
if (rnd.NextDouble() <= (1.0 / count))
{
result = element;
}
++count;
}
return result;
}
Run Code Online (Sandbox Code Playgroud)
在旁注:如果我们使用Linq,我们希望操作使用IEnumerable<T>
一次(并且只使用一次!).现在你知道为什么了.
使其适用于列表和数组
虽然这是一个巧妙的技巧,但如果我们处理a,我们的性能现在将会变慢List<T>
,这没有任何意义,因为我们知道有更好的实现可用,因为索引的属性Count
可供我们使用.
我们正在寻找的是这个更好的解决方案的共同点,我们可以在尽可能多的集合中使用它.我们最终得到的是IReadOnlyList<T>
接口,它实现了我们需要的一切.
因为我们的属性知道是真正的IReadOnlyList<T>
,我们现在可以安全地使用Count
和索引,而不运行崩溃的应用程序的风险.
然而,虽然IReadOnlyList<T>
看起来很吸引人,但IList<T>
由于某些原因似乎没有实现它......这基本上意味着IReadOnlyList<T>
在实践中有点赌博.在这方面,我非常确定IList<T>
有比IReadOnlyList<T>
实现更多的实现.因此,最好只支持两种接口.
这引出了我们的解决方案:
public static T AnyOne<T>(this IEnumerable<T> source)
{
var rnd = new Random();
var list = source as IReadOnlyList<T>;
if (list != null)
{
int index = rnd.Next(0, list.Count);
return list[index];
}
var list2 = source as IList<T>;
if (list2 != null)
{
int index = rnd.Next(0, list2.Count);
return list2[index];
}
else
{
double count = 1;
T result = default(T);
foreach (var element in source)
{
if (rnd.NextDouble() <= (1.0 / count))
{
result = element;
}
++count;
}
return result;
}
}
Run Code Online (Sandbox Code Playgroud)
PS:对于更复杂的场景,请查看策略模式.
随机
@Yannick Motton发表评论说你必须要小心Random
,因为如果你多次调用这样的方法,它就不会是随机的.使用RTC初始化Random,因此如果您多次创建一个新实例,它将不会更改种子.
解决这个问题的一个简单方法如下:
private static int seed = 12873; // some number or a timestamp.
// ...
// initialize random number generator:
Random rnd = new Random(Interlocked.Increment(ref seed));
Run Code Online (Sandbox Code Playgroud)
这样,每次调用AnyOne时,随机数生成器都会收到另一个种子,即使在紧密循环中它也能正常工作.
总结一下:
所以,总结一下:
IEnumerable<T>
应该迭代一次,而且只迭代一次.否则可能会给用户带来意想不到的结果.IReadOnlyList<T>
绝对是最佳候选人,但它并不是从中继承的IList<T>
,而是在实践中不那么有效.最终结果是Just Works.
归档时间: |
|
查看次数: |
1013 次 |
最近记录: |