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

时间:2015-11-26 19:12:45

标签: c# arrays generics unity3d

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

事实上,有没有办法一次性推广涵盖arrayList<> s的泛型?或者知道不可能吗?

对我来说,答案是否可以进一步涵盖Collection?或者确实有下面的专家之一已经实现了??

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

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

6 个答案:

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

请注意:对于 Unity3D 环境,具体来说,这是正确答案。关于这个答案IReadOnlyList<T>的进一步改进,它在Unity3D中不可用。 (关于IEnumerable<T>的(巧妙)扩展甚至覆盖没有计数/可索引性的对象的情况,当然在游戏引擎情况下这将是一个独特的概念(例如AnyOneEvenInefficiently或{{1} })。)

答案 1 :(得分:6)

事实上,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];
}

正如另一个答案所述,IList<T>也有效,但良好的做法要求您向来电者请求方法所需的最小功能,在本例中为{{ 1}}属性和只读索引器。

Count也可以,但它允许调用者传递非集合迭代器,其中IEnumerable<T>Count扩展方法效率非常低 - 如ElementAt,数据库查询等

Unity3D工程师的注意事项:如果你看一下IReadOnlyList Interface documentation‌的最底层,它可以从.Net 4.5开始使用。在早期版本的.Net中,您必须使用Enumerable.Range(0, 1000000)(自2.0起可用)。 Unity在.Net版本上远远落后。 2016年,Unity仅使用.Net 2.0.5。因此,对于Unity3D,您必须使用IList<T>

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

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

  1. 计算来源中的元素数量。如果源是一个简单的IEnumerable<T>,这意味着要遍历列表中的所有元素,如果它是f.ex.一个List<T>,它将使用Count属性。
  2. 重置可枚举,转到元素randomIndex,抓住并返回它。
  3. 这里有两件事可能出错。首先,您的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
        }
    }
    

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

    修复IEnumerable实现

    正如我们所见,IEnumerable<T>实施的问题是您必须经历2次数据。我们可以通过一次浏览数据来解决这个问题。

    &#39;&#39;&#39;这里实际上很简单:如果我们看过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<T>(并且只使用一次!)。现在你知道为什么了。

    使其适用于列表和数组

    虽然这是一个巧妙的伎俩,但如果我们处理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;
        }
    }
    

    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<T>&#39; s应该迭代一次,并且只能迭代一次。否则可能会给用户带来意想不到的结果。
    • 如果您可以访问比简单枚举更好的功能,则无需浏览所有元素。最好立即抓住正确的结果。
    • 考虑一下您要仔细检查的界面。虽然IReadOnlyList<T>绝对是最佳候选人,但它并非继承自IList<T>,这意味着它在实践中效果较差。

    最终结果是Just Works。

答案 3 :(得分:3)

T[]List<T>都共享相同的界面:IEnumerable<T>

IEnumerable<T>但是,没有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);
}

答案 4 :(得分:1)

答案是使用原始代码!

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

鉴于该问题被标记为unity3d,显然他的代码将成为游戏的一部分。在游戏中,您想要的最后一件事是由于garbage collection而导致的间歇性停顿。通常,在Unity中,您希望枚举器具有出色的性能。这使我自己得到了答案:

请勿使用接口进行枚举

除非您真的必须这样做。 List<T>T[]类型具有高度优化的值类型枚举器。将类型转换为接口后,您将还原为未优化的参考类型的版本。每次对GetEnumerator()的非优化版本的调用都会产生垃圾,加起来会导致随后的结结(请相信我),当垃圾收集器收集这些分配的对象时。

  • 优化的版本的List<T>.GetEnumerator() here
  • IEnumerable<T>.GetEnumerator() here的未优化版本。

详细信息,请参阅我的other answer

答案 5 :(得分:0)

您可以稍微更改一下定义:

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<T>接口的所有类型定义扩展方法。