c#generic,包括数组和列表?

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)

事实上,是否有一种方法可以一次性推广覆盖arrays和List<>s的泛型?或者知道不可能吗?


它发生在我身上,答案是否可以进一步包含Collections?或者确实有下面的专家之一已经实现了??


PS,我很抱歉没有明确提到这是在Unity3D环境中."Random.Range"是一个统一到骨骼的功能,"AnyOne"调用读取为任何游戏工程师的100%游戏引擎.这是你为任何游戏项目输入的第一个扩展,并且你经常在游戏代码中使用它("任何爆炸!""任何硬币声音效果!"等等!)

显然,它当然可以在任何c#milieu中使用.

Ric*_*lay 8

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>甚至覆盖没有计数/可索引性的对象的情况,当然在游戏引擎情况下将是一个独特的概念(例如AnyOneEvenInefficientlyAnyOneEvenFromUnsafeGroups).)

  • @Joe IReadOnlyList实际上是更好的选择,因为它不允许写操作(如Add),这是提问者的代码不需要的. (2认同)

Iva*_*oev 8

实际上,您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>也可以工作,但它允许调用者传递非集合迭代器,其中CountElementAt扩展方法可能非常低效 - 比如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>.

  • @JoeBlow 如果您查看 [IReadOnlyList&lt;T&gt; 接口文档](https://msdn.microsoft.com/en-us/library/hh192385(v=vs.110).aspx) 的最底部,您会会看到类似 **.NET Framework** * 4.5 后可用的内容* :( 所以在早期版本中你必须求助于`IList&lt;T&gt;`(* 2.0 后可用*) (2认同)

atl*_*ste 5

一些人选择的方式很有意思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)

现在,这基本上做的是两件事:

  1. 计算源中元素的数量.如果源是一个简单的,IEnumerable<T>这意味着遍历列表中的所有元素,如果它是f.ex. a List<T>,它将使用该Count属性.
  2. 重置可枚举,转到元素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.

  • @IvanStoev LOL :) 说实话,我觉得所有这些琐碎的算法都被命名很愚蠢,我通常只是边走边编...... (2认同)