【问题标题】:c# generic, covering both arrays and lists?c# 泛型,涵盖数组和列表?
【发布时间】:2015-11-26 19:12:45
【问题描述】:

这是一个非常方便的扩展,适用于 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];
}

不幸的是,它不适用于 List&lt;&gt; 的任何东西。这是适用于任何 List&lt;&gt; 的相同扩展名

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

事实上,有没有办法一次性泛化涵盖arrays 和List&lt;&gt;s 的泛型?还是知道不可能?


答案是否甚至(喘气)包括Collections?


PS,我很抱歉没有明确提到这是在 Unity3D 环境中。例如,“Random.Range”只是一个 Unity 调用(显而易见),“AnyOne”是游戏编程中完全典型的扩展或调用。

显然,这个问题当然适用于任何 c# 环境。

【问题讨论】:

    标签: c# arrays generics unity3d


    【解决方案1】:

    事实上,T[]List&lt;T&gt; 之间最适合您的情况的通用接口是 IReadOnlyList&lt;T&gt;

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

    正如另一个答案中提到的,IList&lt;T&gt; 也可以工作,但良好的做法要求您向调用者请求该方法所需的 最小 功能,在本例中为 Count 属性和只读索引器。

    IEnumerable&lt;T&gt; 也可以工作,但它允许调用者传递一个非集合迭代器,其中CountElementAt 扩展方法可能效率非常低 - 例如Enumerable.Range(0, 1000000)、数据库查询等。


    Unity3D 程序员快到 2020 年了:当然,现在 Unity 中提供了现代版本的 .Net!

    【讨论】:

    • 数组就是一切 :) IEnumerable, ICollection, IList, IEnumerable&lt;T&gt;, ICollection&lt;T&gt;, IList&lt;T&gt;, IReadOnlyCollection&lt;T&gt;, IReadOnlyList&lt;T&gt;
    • @JoeBlow 同意,这应该被标记为答案。
    • @JoeBlow 是的......不幸的是,这不仅仅是 Unity3D ......这是因为 IList&lt;T&gt; 没有实现 IReadOnlyList&lt;T&gt;。可能是因为你可以有一个你只能写而不是读的列表的情况。这就是为什么我添加的答案包括这两种情况。对于自定义列表,您可能会遇到同样的麻烦;不幸的是,IList 的实现比 IReadOnlyList 更多。
    • @JoeBlow 如果您查看IReadOnlyList<T> Interface documentation 的最底部,您会看到类似 .NET Framework Ava​​ilable since 4.5 的内容: (所以在早期版本中,您必须求助于IList&lt;T&gt;自 2.0 起可用
    • 不客气,乔。很高兴参与这个有趣的讨论。
    【解决方案2】:

    T[]List&lt;T&gt; 实际上都实现了IList&lt;T&gt;,它提供了枚举、Count 属性和索引器。

    public static T AnyOne<T>(this IList<T> ra) 
    {
        int k = ra.Count;
        int r = Random.Range(0,k);
        return ra[r];
    }
    

    历史记录:在过去的几十年中,这是 Unity3D 的正确且唯一的解决方案,因为在过去,现代 .Net 在 Unity 中不可用。

    【讨论】:

    • 是的,让我想知道为什么会这样。实现IList&lt;T&gt; 的数组违反了里氏替换原则。
    • @Joe IReadOnlyList 实际上是这里更好的选择,因为它不允许写操作(如添加),而提问者的代码不需要。
    • “当你还需要 Count 和一个索引器的时候不是”啊当然;您需要选择具有这些概念的抽象级别——所以也许 IReadOnlyList 是最好的想法?
    • 抛开毫无意义的讨论,@RichardSzalay 您介意将索引器访问权限更改为ra[r]。就目前而言,如果 Random 生成 0,则此代码将引发 IndexOutOfRangeException
    • 嘿,伙计们,我大胆地决定将令牌赏金放在这里。原因是双重的,(1)R.S.首先用IList“揭开”这个长期困难和具有挑战性的问题。 . .对IReadOnlyList 的(出色)改进只是因为这个答案而出现。此外,(2)对于在这里搜索的一百亿游戏程序员(这是用 c# 编写组件时最流行的扩展想法),这确实是“答案”,所以这是一件好事。我感谢大家。
    【解决方案3】:

    有趣的是,有些人选择IEnumerable&lt;T&gt;,而有些人却坚持使用IReadOnlyList&lt;T&gt;

    现在说实话。 IEnumerable&lt;T&gt; 很有用,很有用。在大多数情况下,您只想将此方法放在某个库中,然后将您的实用程序函数扔给您认为是集合的任何内容,然后完成它。但是,正确使用IEnumerable&lt;T&gt; 有点棘手,我将在此处指出...

    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
    }
    

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

    1. 计算源中元素的数量。如果源是简单的IEnumerable&lt;T&gt;,这意味着遍历列表中的所有元素,如果它是 f.ex。 List&lt;T&gt;,它将使用 Count 属性。
    2. 重置可枚举,转到元素randomIndex,抓住它并返回它。

    这里有两件事可能出错。首先,您的 IEnumerable 可能是一个缓慢的顺序存储,并且执行Count 可能会以意想不到的方式破坏您的应用程序的性能。例如,从设备流式传输可能会给您带来麻烦。话虽如此,您很可能会争辩说,当这是该系列的固有特征时,这是可以预料的——而且我个人认为这个论点会成立。

    其次 - 这可能更重要 - 不能保证您的 enumerable 每次迭代都会返回相同的序列(因此也不能保证您的代码不会崩溃)。例如,考虑一下这段看似无害的代码,它可能对测试有用:

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

    第一次迭代(调用Count()),您可能会生成 99 个结果。您选择元素 98。接下来您调用 ElementAt,第二次迭代生成 12 个结果并且您的应用程序崩溃。不酷。

    修复 IEnumerable 实现

    正如我们所见,IEnumerable&lt;T&gt; 实现的问题是您必须遍历数据 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;
    }
    

    附带说明:如果我们使用 Linq,我们希望操作使用 IEnumerable&lt;T&gt; 一次(而且只有一次!)。现在你知道为什么了。

    使其适用于列表和数组

    虽然这是一个巧妙的技巧,但如果我们在 List&lt;T&gt; 上工作,我们的性能现在会变慢,这没有任何意义,因为我们知道,由于 indexing 和 @987654339 的属性,有更好的实现可用@ 可供我们使用。

    我们正在寻找的是这个更好的解决方案的共同点,我们可以在尽可能多的集合中使用它。我们最终得到的是IReadOnlyList&lt;T&gt; 接口,它实现了我们需要的一切。

    由于我们知道适用于IReadOnlyList&lt;T&gt; 的属性,我们现在可以安全地使用Count 和索引,而不会冒应用程序崩溃的风险。

    然而,虽然IReadOnlyList&lt;T&gt; 看起来很吸引人,但IList&lt;T&gt; 出于某种原因似乎并没有实现它......这基本上意味着IReadOnlyList&lt;T&gt; 在实践中有点像赌博。在这方面,我很确定IList&lt;T&gt; 的实现比IReadOnlyList&lt;T&gt; 的实现要多得多。因此,最好只支持这两个接口。

    这将我们引向这里的解决方案:

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

    PS:对于更复杂的场景,请查看策略模式。

    随机

    @Yannick Motton 说你必须小心Random,因为如果你多次调用这样的方法,它就不会是真正随机的。 Random 是用 RTC 初始化的,所以如果你多次创建一个新实例,它不会改变种子。

    一个简单的解决方法如下:

    private static int seed = 12873; // some number or a timestamp.
    
    // ...
    
    // initialize random number generator:
    Random rnd = new Random(Interlocked.Increment(ref seed));
    

    这样,每次调用 AnyOne 时,随机数生成器都会收到另一个种子,即使在紧密的循环中也能正常工作。

    总结一下:

    所以,总结一下:

    • IEnumerable&lt;T&gt; 应该被迭代一次,并且只被迭代一次。否则可能会给用户带来意想不到的结果。
    • 如果您可以获得比简单枚举更好的功能,则无需遍历所有元素。最好立即获得正确的结果。
    • 仔细考虑您正在检查的接口。虽然IReadOnlyList&lt;T&gt; 绝对是最佳候选,但它不是从IList&lt;T&gt; 继承的,这意味着它在实践中的效率会降低。

    最终结果是 Just Works。

    【讨论】:

    • 哎呀,错字...我通常在记事本中写这些答案 :-) 它应该是“计数”。
    • 嗯,非常令人惊讶的是,我以前从未见过“运行机会”算法 - 谢谢!
    • @JoeBlow 它叫Reservoir sampling :)
    • 好一个伊万谢谢。 (顺便说一句,这是一个非常糟糕的 wiki 页面!)
    • @IvanStoev LOL :) 老实说,我觉得所有这些琐碎的算法都被命名很愚蠢,我通常只是边走边编......
    【解决方案4】:

    T[]List&lt;T&gt; 共享同一个接口:IEnumerable&lt;T&gt;

    IEnumerable&lt;T&gt; 但是,没有 Length 或 Count 成员,但有一个扩展方法 Count()。序列上也没有索引器,所以你必须使用ElementAt(int)扩展方法。

    类似的东西:

    public static T AnyOne<T>(this IEnumerable<T> source)
    {
        int endExclusive = source.Count();
        int randomIndex = Random.Range(0, endExclusive); 
        return source.ElementAt(randomIndex);
    }
    

    【讨论】:

    • 赞成良好的解释,以及不使用像 OP 这样无意义的变量名。
    • 嗨 ataravati,看到像你这样完全不识字的人总是让我难过 :) “ra”显然意味着“数组”,它可能是编程中最著名的笑话或有趣的名字。跨度>
    • @JoeBlow,我知道。 “k”也表示计数,“r”表示随机。
    • Yan,您似乎用IEnumerable 揭开了问题的核心,但您现在是否已经成为共识的一部分,认为'IReadOnlyList&lt;T&gt; 是在实际代码中使用的最佳方法现实世界?抱歉,我很难理解这里的微妙之处。
    【解决方案5】:

    答案是使用原代码!

    这应该是StackOverflow 上唯一一个问题,该问题本身显着说明了比提供的任何答案更好的代码。所有建议的答案都鼓励使用接口,这意味着对性能的重大影响。不要在生产代码中使用这些解决方案!

    鉴于问题被标记为unity3d,显然他的代码将成为游戏的一部分。在游戏中,您最不想看到的是由于garbage collection 而导致的间歇性口吃。通常,在 Unity 中,您希望枚举器具有极高的性能。这让我想到了答案本身:

    不要使用接口进行枚举

    除非你真的必须这样做。 List&lt;T&gt;T[] 类型具有高度优化的 value-typed 枚举器。将类型转换为接口后,您将恢复为未优化的引用类型版本。对GetEnumerator() 的非优化版本的每次调用都会产生垃圾,当垃圾收集器收集这些分配的对象时,会增加稍后会发生的卡顿(相信我)。

    • List&lt;T&gt;.GetEnumerator()here优化版本。
    • 未优化版本的IEnumerable&lt;T&gt;.GetEnumerator()here

    详情,见我的other answer

    【讨论】:

    • “答案是使用原始代码” - 哪个问题的“答案”?显然不是 OP,whish 是为了将该代码概括为数组以外的容器类型。其次,通用答案使用 2 个简单的虚拟属性 - Countthis(索引器),因此不会“暗示性能受到重大影响”。最后,这个问题根本不涉及枚举,因此“答案本身”虽然总体上是正确的,但却是在回答此处未提出的问题。
    • @IvanStoev ,虽然您的观点在“此 QA”方面很重要,但此基本信息:一旦您将类型转换为接口,您将恢复为非优化的引用类型版本我觉得非常有价值。
    【解决方案6】:

    你可以稍微改变一下你的定义:

    public static T AnyOne<T>(this IEnumerable<T> ra) 
    {
        if(ra==null)
            throw new ArgumentNullException("ra");
    
        int k = ra.Count();
        int r = Random.Range(0,k);
        return ra.ElementAt(r-1);
    }
    

    现在您为所有实现IEnumerable&lt;T&gt; 接口的类型定义扩展方法。

    【讨论】:

    • 我认为ElementAt 是否确实做到了这一点?
    • 在我写评论的时候,你已经从 OP 复制了索引器 ;-)
    猜你喜欢
    • 2023-03-06
    • 1970-01-01
    • 2012-11-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-10
    • 1970-01-01
    相关资源
    最近更新 更多