65.9K
CodeProject 正在变化。 阅读更多。
Home

适用于 .NET 3.0 LINQ 查询的随机样本扩展方法

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.93/5 (7投票s)

2006年10月31日

CPOL

4分钟阅读

viewsIcon

45699

downloadIcon

178

允许从任何 IEnumerable 集合返回随机样本的扩展方法。

引言

从更大的源中收集随机数据样本有许多用途,包括测试、调试和营销。以下是一些示例:

  • 在开发或调试 LINQ 查询时,更容易分析来自更大源的较小结果集。
  • 在白天运行简化的单元测试以提高多开发人员的集成时间,在工作时间内使用一小部分样本,在夜间构建中使用完整的测试数据。
  • 营销 - "让我们给 10% 的用户打电话,询问他们一些反馈或推荐"

我想能够编写如下查询,从更大的字符串数组中获取 10 个随机名称的样本:

var sample = listOfNames.RandomSample(10);

本文介绍的代码向 IEnumerable 添加了一个扩展方法,该方法允许您从任何类型的集合中生成随机元素样本。

背景 - 扩展方法

C# 的下一个版本将引入一项名为扩展方法的新功能。通常,只要类或其祖先类提供了作用域内且具有该名称的方法,您就可以调用对象实例上的方法。扩展方法允许我们在不更改原始类或创建我们自己的子类派生的情况下“向类型添加”方法。

在下面的示例中,我向 .NET 的 string 类添加了一个新方法,该方法返回一个以 Hello 为前缀的新 string

public static class Extensions
{
    public static string Hello(this string s) {
         return "Hello " + s;
    }
}

创建扩展方法后,我可以使用以下语法将 Hello … 添加到任何 string 实例对象,在这种情况下,将结果 string 写入控制台窗口。

string name = "Troy";
Console.WriteLine(name.Hello());

上述重要的语法更改是第一个参数在参数列表中的类型之前带有 this 修饰符。

定义扩展方法的其他要点是:

  • 扩展方法必须在标记为 static 的类中。
  • 每个扩展方法都必须标记为 static
  • this 修饰符必须放在第一个参数上。

在编译时,C# 编译器首先检查是否存在任何名称和参数签名匹配的实例方法。如果没有找到匹配的方法名称或签名,则搜索将继续通过使用 using 子句导入的任何命名空间。如果存在具有相同名称且第一个参数具有 this 修饰符、类型与实例对象类型相同的 static 方法,则将使用该方法。

我们的 RandomSample 扩展方法将允许任何实现 IEnumerable 的实例对象返回另一个 IEnumerable,其中包含我们请求的随机序列元素的数量。

Using the Code

使用 RandomSequence 扩展方法时,您有两种方法签名可供选择:

[IEnumerable object].RandomSample( count, Allow Duplicates )
[IEnumerable object].RandomSample( count, Seed, AllowDuplicates )

count:要返回的元素数量,如果源列表中的元素少于该数量,则返回的数量也少于该值。

允许重复truefalse。如果为 true,则一个元素可能会被返回多次,如果随机生成器多次选择它。如果为 false,则每个元素最多只返回一次。

Seed:随机序列生成器的初始整数种子。如果您不指定,将使用系统滴答计数。如果您指定显式种子,那么对于给定的相同输入源列表,每次调用将生成相同的序列。这对于能够使用特定随机序列重复测试很有用。

要将此代码用于您的项目,请下载本文的源文件,并在希望调用 RandomSequence 方法的代码中添加以下 using 子句:

using Aspiring.Query;

通过添加此 using 子句,我们的扩展方法现在可用,并且继承自 IEnumerable 的对象可以利用其操作,如下面的代码所示,该代码从列表中返回三个随机名称,允许重复(最后一个参数为 true 表示允许重复,如果随机序列也决定重复;如果为 false,则每个元素只返回一次)。

string[] firstNames = new string[] {"Paul", "Peter", "Mary", "Janet", 
   "Troy", "Adam", "Nick", "Tatham", "Charles" };

var randomNames = firstNames.RandomSample(3, true);

foreach(var name in randomNames) {
    Console.WriteLine(name);
}

这是实现我们 RandomSample 扩展方法以用于 IEnumerable 对象的代码:

using System;
using System.Collections.Generic;
using System.Text;
using System.Query;

namespace Aspiring.Query
{
    public static class RandomSampleExtensions
    {
        public static IEnumerable<T> RandomSample<T>(
           this IEnumerable<T> source, int count, bool allowDuplicates) {
           if (source == null) throw new ArgumentNullException("source");
           return RandomSampleIterator<T>(source, count, -1, allowDuplicates);
        }

        public static IEnumerable<T> RandomSample<T>(
        this IEnumerable<T> source, int count, int seed, 
           bool allowDuplicates)
           {
           if (source == null) throw new ArgumentNullException("source");
           return RandomSampleIterator<T>(source, count, seed, 
               allowDuplicates);
        }
 
        static IEnumerable<T> RandomSampleIterator<T>(IEnumerable<T> source, 
            int count, int seed, bool allowDuplicates) {
            
            // take a copy of the current list
            List<T> buffer = new List<T>(source);

            // create the "random" generator, time dependent or with 
            // the specified seed
            Random random;
            if (seed < 0)
                random = new Random();
            else
                random = new Random(seed);

            count = Math.Min(count, buffer.Count);

            if (count > 0)
            {
                // iterate count times and "randomly" return one of the 
                // elements
                for (int i = 1; i <= count; i++)
                {
                    // maximum index actually buffer.Count -1 because 
                    // Random.Next will only return values LESS than 
                    // specified.
                    int randomIndex = random.Next(buffer.Count); 
                    yield return buffer[randomIndex];
                    if (!allowDuplicates)
                        // remove the element so it can't be selected a 
                        // second time
                        buffer.RemoveAt(randomIndex);                         
                }
            }
        }
    }
}

关注点

编写 LINQ 扩展方法的核心是 .NET 2.0 的 yield return 关键字。每次框架调用 GetNext() 枚举器方法(在 ForEach 语句的每次循环中都会这样做),我们的例程将从前一个 yield return 语句之后的行开始执行。框架会在调用之间维护状态,因此创建像这样的有趣枚举器所需的工作量将是 .NET 1.1 所需工作量的一小部分。

我编写了一个包含更多有用扩展方法的库,并将它们发布在我的博客上。但是,它们都遵循相同的模式:扩展 IEnumerable 并使用 yield return 语句构建迭代器模式。

© . All rights reserved.