如何从 C# 中的泛型类型数组中选择一组随机值?

Posted

技术标签:

【中文标题】如何从 C# 中的泛型类型数组中选择一组随机值?【英文标题】:How to pick set number of random values from a generic type array inC#? 【发布时间】:2020-05-22 08:38:18 【问题描述】:

我想从泛型类型数组中选择一组随机值(比如说 2 个值)。我设法实现了它,但出于某种原因,它有时会返回重复的数组,例如:[20.33, 20.33] 或 [32, 32] 或 ["Rugby", "Rugby"]。知道如何解决这个问题吗?

请注意,我是从 json 对象中获取此数组的,因此使用的是 Newtonsoft.Json JArray。

var arr = [ 0.4, 20.33, 76.01, 47.3, 23.78];
//or
var arr = [ 32, 68, 89, 27, 93];
// or 
var arr = [ "Football", "Rugby", "Cricket", "Tennis", "Basketball"];


var count = 2;
var item = new JArray();

foreach (var m in Enumerable.Range(0, count).Select(i => arr[new Random().Next(arr.Count())]))

  if (!item.Contains(m))
  
     item.Add(m);
  

【问题讨论】:

“最佳”方法取决于数组中有多少值以及您要选择多少。如果它只有几个,那么您可以制作一个数组的副本并将其打乱(或等效)并从中获取前 N 个值。如果数组中有很多项,而您只想要其中的几个,则有更好的方法。 另请注意,您正在为每次迭代初始化 Random。这可能会增加重复的机会,因为 Random 是使用基于系统时钟的相同种子值初始化的。更好的方法是初始化 Random 一次并重用实例 @AnuViswan 对于 .Net Framework 确实如此 - 但请注意,对于 .Net Core 3.x,该特定问题已得到修复。但即使使用 .Net Core,我也同意 OP 应该在循环外创建 Random! 旁注:如果是asp.net,我们应该注意线程安全Random 不是线程安全的,这就是为什么天真static Random s_Random = new Random(); 不是出路)。 devblogs.microsoft.com/pfxteam/… 除了线程安全问题之外,循环中还有一个根本性的缺陷。它只迭代count 次,但如果其中一项因已在输出数组中而被拒绝,则循环将在添加正确数量的项之前终止。 【参考方案1】:

根据JArray 类的实现(参见source code),其方法Contains 使用引用相等来检查指定项是否包含在数组中。因此这段代码

if (!item.Contains(m))

    item.Add(m);

允许将重复项添加到item 集合中。尝试使用List<object> 而不是JArray。此外(正如其他人在 cmets 中提到的那样)您应该在 linq 表达式之外创建 Random 对象以生成不同的数字。尝试像这样重写您的代码:

var count = 2;
var temp = new List<object>();
// Will generate random numbers.
var random = new Random();

foreach (var m in Enumerable.Range(0, count).Select(i => arr[random.Next(arr.Count())]))

    // Ensures that duplicates will not be added.
    if (!temp.Contains(m))
    
        temp.Add(m);
    


// Add generated items into the JArray and then use it.
var item = new JArray(temp);

【讨论】:

【参考方案2】:

这篇文章中关于 Random 类的工作原理和 lock 语句似乎有些混乱。

我希望通过这个答案稍微澄清一下。

首先让我们复现问题:

private void Button_Click(object sender, RoutedEventArgs e)

  var result = new StringBuilder(1000);

  for(int i = 0; i < 10; i++)
  
    //we are creating a new instance 
    //every single iteration
    //this means all these instances
    //have the same 'seed'
    var rand = new Random();

    result.AppendLine(rand.Next().ToString());
  

  //produces a lot of duplicates
  MessageBox.Show(result.ToString());

输出:

1467722682
1467722682
1467722682
1467722682
1467722682
1467722682
1467722682
1467722682
1467722682
1467722682

上面的代码产生了很多重复! 很多实例都有相同的“种子”。

现在让我们测试一些其他答案中关于使用“锁定”语句“修复”问题的声明:

private void Button_Click(object sender, RoutedEventArgs e)

  var result = new StringBuilder(1000);

  for(int i = 0; i < 10; i++)
  
    //we are creating a new instance 
    //every single iteration
    //this means all these instances
    //have the same 'seed'

    //now using a lock
    lock(this)
    
      var rand = new Random();
      result.AppendLine(rand.Next().ToString());
            
  

  //produces a lot of duplicates
  //locking did NOTHING to fix the problem.
  MessageBox.Show(result.ToString());

输出:

2013405475
2013405475
2013405475
2013405475
2013405475
2013405475
2013405475
2013405475
2013405475
2013405475

如您所见,锁定无济于事解决问题!

现在让我们看看问题是如何解决的:

private void Button_Click(object sender, RoutedEventArgs e)

  var result = new StringBuilder(1000);

  //we are creating a new instance 
  //outside of the for loop     
  var rand = new Random();

  for(int i = 0; i < 10; i++)
                  
    result.AppendLine(rand.Next().ToString());
  

  //no duplicates
  //the problem is now fixed
  MessageBox.Show(result.ToString());

输出:

855346142
1488935613
141810032
1396703820
703238132
978249590
138102129
2143944359
224694938
1542730216

正如您现在所见,我们没有重复

实际的问题是 Random 类使用系统的时钟来创建种子,而您创建多个实例的速度非常快,它们都具有相同的种子。

我们看一下Random类的documentation:

默认种子值来自系统时钟,其分辨率有限

这正是问题所在,但让我们从同一份文档中了解微软对此的看法:

通过调用无参数构造函数连续创建的随机对象将具有相同的默认种子值,因此将产生相同的随机数集

是的,这是对正在发生的事情的准确描述......

那么微软对于如何解决这个问题是怎么说的:

可以通过使用单个 Random 对象生成所有随机数来避免这个问题

这正是我们将实例化移出 for 循环时所做的。

关于此的最后一点说明:

请注意,此限制不适用于 .NET Core。

根据微软的说法,如果你使用 .NET Core,这个问题就不会再发生了。 它只影响 .NET Framework!

更新:

根据@Aristos 的说法,如果代码包含在经典的 ASP.NET 应用程序中,其行为会有所不同。

我刚刚在 ASP.NET 应用程序中尝试了完全相同的示例,得到了完全相同的结果。

正如我所料,这个问题与 ASP.NET 无关。

【讨论】:

评论不用于扩展讨论;这个对话是moved to chat。【参考方案3】:

由于 Random 的实现方式,您必须确保使用“lock”调用 random。在多用户环境下可以同时调用random和内部的静态值或者初始化random的值相同。

例如,当随机开始时,它使用初始化时间 - 如果你同时调用它,你会得到相同的结果。这是随机数的初始化 - 注意使用的 TickCount 并基于该数字生成器给出结果。相同的初始化后面的所有数字序列都是一样的。

[__DynamicallyInvokable]
public Random() : this(Environment.TickCount)


如何解决这个问题。使用一个静态随机引用,它是由静态类创建的,并且每次获得下一个值时也使用lock

所以你的代码将是

var arr = [ 0.4, 20.33, 76.01, 47.3, 23.78];
//or
var arr = [ 32, 68, 89, 27, 93];
// or 
var arr = [ "Football", "Rugby", "Cricket", "Tennis", "Basketball"];


var count = 2;
var item = new JArray();

foreach (var m in Enumerable.Range(0, count).Select(i => arr[GlobalFunctions.RandomInt(arr.Count())]))

  if (!item.Contains(m))
  
     item.Add(m);
  

基于这个对象

public static class GlobalFunctions

    static Random random = null;
    private static readonly object syncLock = new object();

    static GlobalFunctions()
    
        lock (syncLock)
        
            if (random == null)
                random = new Random();
        
       

    public static int RandomInt(int max)
    
        lock (syncLock)
        
            return random.Next(max);
        
     

为什么我们还需要锁定 Next - 因为如果你看到 Next 的内部调用,你最终会使用对象的一些内部值 - 所以你还需要同步每个调用以避免重复 -注意this.inextthis.inextp - 查看random.next的内部:

[__DynamicallyInvokable]
public virtual int Next()

    return this.InternalSample();

private int InternalSample()

    int inext = this.inext;
    int inextp = this.inextp;
    if (++inext >= 0x38)
    
        inext = 1;
    
    if (++inextp >= 0x38)
    
        inextp = 1;
    
    int num = this.SeedArray[inext] - this.SeedArray[inextp];
    if (num == 0x7fffffff)
    
        num--;
    
    if (num < 0)
    
        num += 0x7fffffff;
    
    this.SeedArray[inext] = num;
    this.inext = inext;
    this.inextp = inextp;
    return num;

底线 - 使用 lock 同步 Random 对象。

用示例代码证明

有时候很难识别这个问题,因为您必须有很多用户访问您的网站,即使这样您也可能永远不会看到他们看到相同的结果。

现在,为了证明我在一个页面上使用 iframe 创建了许多用户模拟,这会调用另一个调用生成的页面。结果不言自明——您也可以下载示例代码自行检查。

其他建议 - 如果两个或多个用户同时调用相同的页面/处理程序/函数,我们可能会有重复

现在有了锁和静态随机我们没有重复

以及测试它的代码 http://planethost.gr/so/RandomTestCode.rar

【讨论】:

如果您使用多个线程而不是这种情况,您只需要锁定syncRoot。事实上,在单线程代码上使用“锁”是无用且错误的。为什么要锁定单线程代码? OP 甚至没有使用静态字段。而且您似乎不了解“锁定”的用法。锁的使用与“静态”字段无关。即使没有静态字段,也需要锁来为线程之间的共享对象创建同步上下文。因此,只有在跨不同线程共享对象时,lock 语句才在多线程代码中有意义......它与“多用户”环境无关。 @JonathanAlfaro 它的 asp.net(多用户调用,默认情况下为多个线程,因为它们可以被多个用户同时调用) - 并且 OP 有重复 - 用户与多线程无关......在 ASP.NET 中,请求由多个线程处理,而与用户无关......但他的代码不是多线程的,因为它不使用多个线程......你会使用锁的唯一原因是如果正在访问的对象是跨多个线程共享的......为此,对象的范围必须位于方法的外部......例如在类级别...在这种情况下,他正在使用的 Random 类的实例没有被共享...您确实需要阅读有关锁和线程同步的内容。 OP 有重复项,因为他在循环的每次交互中创建一个新的 Random 实例,这导致一些实例具有相同的种子。在您发布的这行代码中“public Random() : this(Environment.TickCount)”TickCount 的分辨率约为 16 毫秒,这意味着如果您在 16 毫秒内创建两个随机实例,一个紧接着另一个实例将返回完全相同的序列,因为它们都具有相同的种子。这不能通过使用锁来解决......因为实例已经按顺序而不是同时创建。

以上是关于如何从 C# 中的泛型类型数组中选择一组随机值?的主要内容,如果未能解决你的问题,请参考以下文章

C#中的泛型是啥意思?

C#泛型编程

数组中的泛型联合

C#泛型实例详解

泛型的泛型的好处

在 C# XML 文档中引用泛型类型的泛型类型?