【问题标题】:How to replace FirstOrDefault with something like RandomOrDefault to "balance" calls?如何用 RandomOrDefault 之类的东西替换 FirstOrDefault 以“平衡”调用?
【发布时间】:2011-08-05 08:17:43
【问题描述】:

我有这样的代码

    senders.FirstOrDefault(sender => !sender.IsBusy);

这条线经常被调用。

问题是它总是返回第一个不忙的sender;经常返回相同的第一个发件人,但很少返回最后一个发件人。如何轻松平衡?

理想情况下,在每次通话时,我都应该返回最不常用的发件人。 IE。在所有非忙碌发件人之间选择在最后一秒内被选中次数最少的发件人。

【问题讨论】:

  • 你想要伪随机还是最不忙?最不忙意味着您保留谁忙谁不忙的历史记录,并参考该历史记录来确定一段时间内最不忙的发件人。
  • 伪随机将是一个很大的改进,但是最好保留历史记录,因为报告“负载”(每秒发送数)会很好,如果可以的话也很好“限制”负载
  • 您有很多建议使用RandomOrDefaultShuffle 扩展方法的答案。如果您使用这些解决方案中的任何一个,请记住尽快过滤掉尽可能多的发件人:senders.Shuffle().FirstOrDefault(s => !s.IsBusy)(不好,因为不需要对忙碌的发件人进行洗牌)vs. senders.Where(s => !s.IsBusy).RandomOrDefault()(好)。理想情况下,你应该有一个扩展方法可以做到这两个:senders.RandomOrDefault(s => !s.IsBusy)
  • 检查我的非随机方法的解决方案
  • @stakx,我认为您的评论并非 100% 准确。如果您的 RandomOrDefault 方法采用谓词,则在 RandomOrDefault 之前调用 Where 是没有意义的...哦,抱歉错过了提到这一点的评论的最后一部分。

标签: c# linq random


【解决方案1】:

可能是这样的:

public static T RandomOrDefault<T>(this IEnumerable<T> dataSet)
{ 
    return dataSet.RandomOrDefault(y => true);
}

public static T RandomOrDefault<T>(this IEnumerable<T> dataSet, Func<T, bool> filter)
{
    var elems = dataSet.Where(filter).ToList();
    var count = elems.Count;
    if (count == 0)
    {
        return default(T);
    }
    var random = new Random();
    var index = random.Next(count - 1);
    return elems[index];
}

然后你可以调用它:

senders.RandomOrDefault(sender => !sender.IsBusy);

【讨论】:

  • +1。这样的扩展方法会导致代码可能无法更具表现力和更易于理解。
  • 这个算法在序列上迭代 3 次。还不错,但这只需 1 次迭代即可实现。顺便说一句,dataSet.Length 有语法错误,我想应该是 dataSet.Count()
  • @Dmitry Lobanov,3 次?我认为是两个,一个在调用两个列表时调用,一个在我获取元素时调用,而且我还认为由于我正在接受IEnumerable&lt;T&gt;,所以在不到两个的时间内完成它可能会更难,但我可能是错的。我的意思是你必须得到计数并从IEnumerable 得到计数,你必须迭代它,还是我错了?请提出您的建议。
  • @Dmitry Lobanov,没有先看到你的答案。很好的解决方案。但考虑到复杂性,我的解决方案也是 O(n)。我什至认为我可以将elems.ElementAt 更改为elems[index],这将使其成为一个常量操作=> 对元素进行一次迭代。您的解决方案的不同之处在于,我使用 ToList 将所有内容复制到内存中。
  • 是的,如果你在IEnumerable&lt;T&gt; 上调用Count(),你会遍历它。如果您在 IEnumerable 上调用 ElementAt(),则同样有效。但是,当实际类型类似于 ICollection(带有 Count 属性)或 IList(带有 indexer 属性)时,LINQ 中必须进行一些优化。
【解决方案2】:

如果您想有效地获得最少使用的一个,您可能会很好地使用以下非 Linq“列表轮换”解决方案,它是 O(n) 效率和 O(1) 空间,与大多数其他解决方案不同:

// keep track of these
List<Sender> senders;
int nSelected = 0;          // number of already selected senders

// ...

// solution
int total = senders.Count;  // total number of senders
// looking for next non-busy sender
Sender s = null;
for (int i = 0; i < total; i++)
{
    int ind = (i + nSelected) % total; // getting the one 'after' previous
    if (!senders[ind].IsBusy)
    {
        s = senders[ind];
        ++nSelected;
        break;
    }
}

当然,这会在 senders 容器上添加必须可索引的约束。

【讨论】:

  • 我喜欢那个,但我怎样才能“度量”负载呢? IE。我想要“最后一秒发送了多少次”在线统计信息……嗯……但是有了这样的统计数据,我就可以找到最不常用的发件人……
  • @Grozz,修复了代码中的一个小错误:它应该选择 non-busy 发件人。
  • @stakx:谢谢。 @javapowered:您需要每个发件人的总金额还是金额?后者应该大致相同,所以可能是总的,因为您可以在增加 nSelected 的位置添加时间跟踪。
  • @Grozz 我认为我需要每个发件人的统计数据来诊断可能的问题(网络延迟等)。例如,如果一个发件人“滞后”它应该更频繁地“忙碌”,然后导致更少的数据处理
  • @Grozz 为什么不用nSelected = ind + 1 替换++nSelected
【解决方案3】:

您可以通过新的 Guid 轻松地重新排序,如下所示:

senders.Where(sender => !sender.IsBusy).OrderBy(x => Guid.NewGuid()).FirstOrDefault();

您不必弄乱随机数字,也不必为这些数字确定“范围”。我认为它很简单,而且非常优雅。

【讨论】:

  • 我确信这在实践中是可行的,但我不推荐它,原因有以下三个:1. GUID 的创建成本可能比伪随机数高。这本身不会那么糟糕,但是:2. GUID 不一定是随机的。我见过一批非常相似的“顺序”GUID,它们不能用于洗牌集合。 3. 按 GUID 排序阻碍了这段代码的真正含义,使其更难理解。
  • 我可以争辩说 1- 正确,但必须在上下文中评估费用(它可以从微不足道到机器杀戮,只有实际的开发人员会知道) 2 - 它们不是随机的,如果您生成顺序指南。使用 Guid.NewGuid() 会给你随机的指导。认为我在不知道结果的情况下发布了这个? ;) 3 - 单个 OrderBy(x => Guid.NewGuid()) 如何比多 4-5 行代码或调用其他方法更难理解?
  • 关于 3..OrderBy(x =&gt; Guid.NewGuid()) 对我来说似乎更难理解,因为 GUID 不是问题域的一部分(即选择不忙的发件人),但是突然出现在查询中。阅读该代码(“按随机 GUID 排序”)的人可能很容易分心思考,GUID 与此有什么关系? OTOH,像.RandomOrDefault(sender =&gt; !sender.IsBusy) 这样的东西几乎确切地说明了任务的全部内容:“给我(如果可以的话)一个不忙的随机发件人。”哪个更接近问题? -- 1.2.:很公平。 :)
  • (SO 似乎在 cmets 的开头吞下了 @Matteo 的每个实例。我发布此评论是为了让您得到通知。)
  • @stakx 在这种情况下,您可以简单地创建一个扩展方法 Shuffle() 或任何您想在内部执行新 guid 的方法。可读性和域问题已解决:)
【解决方案4】:

您可以在 FirstOrDefault 之前使用 this post 中的“Shuffle”扩展方法

【讨论】:

    【解决方案5】:

    您可以使用一个随机数小于非忙碌发件人总数的跳过。

    senders.Where(sender => !sender.IsBusy).Skip(randomNumber).FirstOrDefault();
    

    虽然确定随机数的合理限制可能有点棘手。

    【讨论】:

      【解决方案6】:

      查找您使用过的发件人及其使用时间。

      var recentlyUsed = new Dictionary<Sender, DateTime>();
      var sender = senders.FirstOrDefault(sender => !sender.IsBusy && (!recentlyUsed.ContainsKey(sender) || recentlyUsed[sender] < DateTime.Now.AddSeconds(-1)));
      if (sender != null)
          recentlyUsed[sender] = DateTime.Now;
      

      【讨论】:

        【解决方案7】:

        基于the "Real world functional programming" book 的算法,这里是从 IEnumearble 获取随机值或默认值的扩展方法的 O(n) 实现。

        public static class SampleExtensions
        {
            // The Random class is instantiated as singleton 
            // because it would give bad random values 
            // if instantiated on every call to RandomOrDefault method
            private static readonly Random RandomGenerator = new Random(unchecked((int)DateTime.Now.Ticks));
        
            public static T RandomOrDefault<T>(this IEnumerable<T> source, Func<T, bool> predicate)
            {
                IEnumerable<T> filtered = source.Where(predicate);
        
                int count = 0;
                T selected = default(T);
                foreach (T current in filtered)
                {
                    if (RandomGenerator.Next(0, ++count) == 0)
                    {
                        selected = current;
                    }
                }
        
                return selected;
            }
        
            public static T RandomOrDefault<T>(this IEnumerable<T> source)
            {
                return RandomOrDefault(source, element => true);
            }
        }
        

        这是确保该算法真正给出均匀分布的代码:

        [Test]
        public void TestRandom()
        {
            IEnumerable<int> source = Enumerable.Range(1, 10);
            Dictionary<int, int> result = source.ToDictionary(element => element, element => 0);
            result[0] = 0;
        
            const int Limit = 1000000;
            for (int i = 0; i < Limit; i++)
            {
                result[source.RandomOrDefault()]++;
            }
        
            foreach (var pair in result)
            {
                Console.WriteLine("{0}: {1:F2}%", pair.Key, pair.Value * 100f / Limit);
            }
        
            Console.WriteLine(Enumerable.Empty<int>().RandomOrDefault());
        }
        

        TestRandom 方法的输出是:

        1: 9,92%
        2: 10,03%
        3: 10,04%
        4: 9,99%
        5: 10,00%
        6: 10,01%
        7: 9,98%
        8: 10,03%
        9: 9,97%
        10: 10,02%
        0: 0,00%
        0
        

        【讨论】:

        • 此算法仅对序列进行一次迭代,这在某些情况下可能很重要,例如迭代网络流或文件。
        • 感谢您的建议,是不是比 Grozz 的解决方案更好? (更短更易读)
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-09-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多