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






2.93/5 (7投票s)
允许从任何 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
:要返回的元素数量,如果源列表中的元素少于该数量,则返回的数量也少于该值。
允许重复
:true
或 false
。如果为 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
语句构建迭代器模式。