LINQ

  说好要解答的,所以就来吧,这是当下最佳解,以后搞不好我有新的解法也不一定。


  在开始之前,我们需要一个类(Person)及一个函数 (GenerateFixedPersons),为了量测性能,这里还有一个Benchmark函数。

//help functions
public class Person
{
    private static Person _empty = new Person();
    public static Person Empty => _empty;

    public string Name { get; set; }
    public int Age { get; set; }
    public int Salary { get; set; }
    public int DepartmentID { get; set; }
}

static List GenerateFixedPersons(int max)
{
    const string characters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    string[] citys =
       {
         "Taipei",
         "NewYork",
         "London",
         "Tokyo"
    };
    var random = new Random();
    return Enumerable.Repeat(Person.Empty, max).Select(s =>
           {
             return new Person()
             {
               Name = new string(Enumerable.Repeat(characters, 10).Select(a => a[random.Next(a.Length)]).ToArray()),
               Age = random.Next(100)
             };
           }).ToList();
}

static void Benchmark(Action action, string label)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    action();
    sw.Stop();
    Console.WriteLine($"benchmark for {label}");
    Console.WriteLine($"{sw.ElapsedMilliseconds} ms");
}

1. LINQ Where与foreach

   如下图,哪个比较快?

  这是一个观念题,适用于大多数情况,有了GenerateFixedPersons跟Benchmark两个函数,我们可以轻易地解出答案。

 //item 1
static void SearchWithForeach(List data)
{
    foreach (var item in data)
    {
         if (item.Age > 100)
         {
             Console.WriteLine($"{item.Name}, Age:{item.Age}");
             return;
         }
    }
    Console.WriteLine("not found");
}

static void SearchWithLINQ(List data)
{
    var item = data.FirstOrDefault(a => a.Age > 100);
    if (item != null)
        Console.WriteLine($"{item.Name}, Age:{item.Age}");
    else
        Console.WriteLine("not found");
}

static void Main(string[] args)
{
    var list = GenerateFixedPersons(1500000);
    list.Add(new Person() { Name = "c", Age = 101 });;

    Benchmark(() => SearchWithForeach(list), "foreach");
    Benchmark(() => SearchWithLINQ(list), "linq");

    Console.ReadLine();
}

结果如下图。

问题不在结果,结果大概可以猜出来,问题是为什么?很简单,LINQ基础是delegate,也就是委派,调用委派就跟调用另一个函数一样,在C#,委派的成本多数情况下会比函数高一些,因为C#编译器会对函数进行inline,而委派的inline复杂度很高,所以几乎不会进行inline,

那这是不要用LINQ的意思吗?当然不是,用LINQ可以帮助减少程序,在框架下甚至可以减少错误发生的机会,这样的交换是值得的,这题的目的只是要提醒委派成本的存在,如果日后这个差距成为负担,有个方向可以走。

委派有成本,LINQ也有

2.   在下图的GenerateFixedPersons产生出500000笔数据,Age会是1-100之间的随机数,在 //calll 处分别调用两个函数,哪个比较快?为什么?

这是个行为题,呈现FirstOrDefault与LastOrDefault的行为,要验证很简单,丢进Benchmark函数即可。

static void Main(string[] args)
{
    var list = GenerateFixedPersons(1500000);
    list.Add(new Person() { Name = "c", Age = 101 });;

    Benchmark(() => SearchWithLINQ(list), "firstordefault");
    Benchmark(() => SearchWithLastOrDefault(list), "lastordefault");

    Console.ReadLine();
}

差距的原因是LINQ基于Enumerator设计,只往前不往后,因此LastOrDefault的行为必然是从头开始找,保存符合条件的元素,再继续往下比对,如果再发现符合条件的,就保存并丢弃上一个符合条件的,所以一定是跑到最后一个元素,相较于FirstOrDefault在第一个符合条件元素就回传并结束的行为,自然是慢多了。

但这有一个例外,当LastOrDefault没有条件时,会先判断传入的元素集是否支持IList,是的话会用Index跟Length来取值,下例可证明这点。

 Benchmark(() => SearchWithLINQ(list), "firstordefault");
 Benchmark(() =>
          {
            var item = list.LastOrDefault();
            Console.WriteLine($"{item.Name}, Age:{item.Age}");
          }
   , "lastordefault");

LastOrDefault有条件时,会从头找到尾端

3.有方法可以加快这个程序的搜寻性能吗?

这是应用题,Where跟FirstOrDefault带条件时,都是从头一个个找,当需要更快的搜寻方式时,算法是必要的,如果不是很注重性能的,至少要把Dictionary摆出来。

static void SearchWithDictionary(ILookup data, int age)
{
    var item = data[age / 10].FirstOrDefault(a => a.Age > age);
    if (item != null)
        Console.WriteLine($"{item.Name}, Age:{item.Age}");
    else
        Console.WriteLine("not found");
}

static void Main(string[] args)
{
    var list = GenerateFixedPersons(1500000);
    list.Add(new Person() { Name = "c", Age = 101 });
    var lookup = list.ToLookup(a => a.Age / 10);
    int index = 1;
    do
    {
        if (int.TryParse(Console.ReadLine(), out int val))
        {
            SearchWithDictionary(lookup, val);
            index++;
        }
        else
            Console.WriteLine("wrong input");
    }
    while (index < 3);            

    Console.ReadLine();
 }
LINQ很方便,但不能成为不鸟算法的理由

4. 假设传入值是同一个,这两个函数的结果一样吗?性能一样吗?为什么?

测一下就知道了。

static void Main(string[] args)
{
   var list = GenerateFixedPersons(500000);
   list.Add(new Person() { Name = "c", Age = 101 });
   Benchmark(() => UseOrderByChain(list), "chain");
   Benchmark(() => UseOrderByThenBy(list), "thenby");

   Console.ReadLine();
}

结果。

一样是行为题,结果一定不一样,问题在性能,一般都知道OrderBy后面要接ThenBy,但双层OrderBy的例子会带出一个问题,那就是OrderBy如何实践的? 很简单,就是当LINQ运算式的结果需要被取出第一个值时,就会进行排序,形成一个暂存数组,然后吐出值来,在双层OrderBy的情况下,foreach是由最后的OrderBy取值,此时最后的OrderBy要向前面的运算式取值,也就是上一个OrderBy,上一个OrderBy进行排序后吐值给最后一个OrderBy再进行排序,所以自然是比较慢,要证明这点,只要在后方的OrderBy下中断点就可以了,其值一定是前面那个OrderBy排好的。

LINQ运算式会在取值开始执行,也就是Delay Execution,最后的要向前面取值,以此类推直到起点。

5.Min跟Max每次都是重头跑过,所以若要取得一个元素集的Min跟Max,那么就得跑两次,如下

有没有方法可以只跑一次循环?性能会比较好吗?为什么?

这个题目有两个点,一个是观念题,循环跑一次会比跑两次快吗?如果循环内的程序相同,自然是无庸置疑一次比较快,但LINQ是基于Delegate设计,把委派成本考虑进去就不一定了,另一个点是怎么只跑一次,LINQ中的Aggregate函数可以帮忙。

static void UseMinMax(List data)
{
    var min = data.Min(a => a.Age);
    var max = data.Max(a => a.Age);
    Console.WriteLine($"Min: {min} Max: {max}");
}

static void CombinMinMaxWithAggregate(List data)
{
    var result = data.Select(a => a.Age).Aggregate(new
    {
         Min = int.MaxValue,
         Max = int.MinValue
    },
    (x, y) => new
    {
         Min = Math.Min(x.Min, y),
         Max = Math.Max(x.Max, y)
    });
    Console.WriteLine($"Min : {result.Min}, Max: {result.Max}");
 }

       

 static void Main(string[] args)
 {
    var list = GenerateFixedPersons(500000);
    list.Add(new Person() { Name = "c", Age = 101 });
    Benchmark(() => UseMinMax(list), "Min&Max");
    Benchmark(() => CombinMinMaxWithAggregate(list), "Aggregate");

    Console.ReadLine();
}

结果。

看完第一题后,应该对这个结果不意外,就是委派成本,如果真的需要拿回这些性能,foreach是最简单直觉的实践方式。

不要错误解读,LINQ是减少程序,进一步以框架来减少犯错机会。

委派要成本,委派要成本,委派要成本

6.这两个函数的结果一样吗?行为一样吗? 为什么?

观念题,结果是一样的,过程不是,用反组译工具看一下。

C#编译器面对Queryable时,会把所有的LINQ 函数调用转成Expression,然后丢给LINQ Query Provider解译及执行,以此例来说,这个LINQ Query Provider就是EnumerableQuery,这是内建的LINQ Query Provider,执行目标则是Enumerable,就过程而言,转译为Expression的动作是在编译器完成,所以理论上不会造成负担,但解译是Runtime处理,会造成一些负担。

你应该不会这样写吧?其实我有这样写过,当碰触LINQ Query Provider时候,什么事都有可能发生。

Queryable是另一个世界,会改变所有LINQ行为。

7. 用LINQ实践Find Text in Files(在目录中寻找包含指定字符串的文件,例如.txt、.ini)的功能

 这是应用题。

static void FindTextInFiles()
{
    var result = from s1 in Directory.GetFiles(@"C:Windows", "*.ini")
                 let content = File.ReadAllText(s1)
                 where content.Contains("drivers")
                 select s1;
    foreach (var item in result)
    {
        Console.WriteLine(item);
    }
}
任何Enumerable都可以用LINQ处理。

8. 这个函数可以正常执行,但有更好的做法吗?

这是个发想题,如果你只是想知道元素集有没有符合条件的,没有进一步动作,用Any。

static void CheckExistsUseAny(List data)
{
    if (data.Any(a => a.Age > 100))
        Console.WriteLine("found");
    else
        Console.WriteLine("not found");
}

就概念上来说,它跟FirstOrDefault性能差不多,但进一步思考,如果面对的是structure,FirstOrDefault是会回传默认值的,用它来对structure类的元素集判断特定元素存不存在是有风险的。

不要把Count()拿出来用,它跟LastOrDefault一样,带条件时会跑到尾端。

9. 有没有方法可以简化双层foreach成为一层?

 如果没看清楚题目的话,这是陷阱题(说好不放陷阱的),因为WriteLine会印出部门及成员名称,所以要用到SelectMany的另一个重载函数。

public class Department
{
    public int ID { get; set; }
    public string Name { get; set; }
    public List Members { get; set; }
}

static void SelectManyForHierarchy()
{
    Department[] list =
    {
        new Department()
        {
             Name = "RD",
             Members = new List()
             {
                 new Person() { Name = "mary"},
                 new Person() { Name = "mark"}
             }
        },
        new Department()
        {
             Name = "Sales",
             Members = new List()
             {
                new Person() { Name = "brain"},
                new Person() { Name = "tobe"}
             }
        }
    };

    foreach (var item in list.SelectMany(a => a.Members,
             (dep, member) =>
               new
               {
                  Dep = dep,
                  Member = member
               })) 
    {
        Console.WriteLine($"Dep: {item.Dep.Name}, Member: {item.Member.Name}");
    }
}

如果不想看到这么科技感的写法,用LINQ Expression写法可以写吗?出奇的简单哦。

我是空格

foreach (var item in from s1 in list
                     from s2 in s1.Members
              select new
              {
                    Dep = s1,
                    Member = s2
              })
{
      Console.WriteLine($"Dep: {item.Dep.Name}, Member: {item.Member.Name}");
}

LINQ有很多重载函数,不要忘记它们。

10. 这个例子中,在印出RD部门的薪资总额时,请问Sales的薪资总额计算好了吗?为什么?

这是观念题,答案是没有,基于延迟执行原则,从最后开始取值。

11. 这是一个大题目,如下例。

switch的缺点是定型,以后如果AlterType增加,就要回到AlterNow增加case区段,有没有方法可以省掉这个switch,达到增加AlterType不用修改AlterNow?

这是一个扩散应用题,通常能解出来就有一定的水准,解的过程中可以看出架构设计能力,在以下的中等解也能看出对于LINQ的理解度。

static IDictionary mapping = null;

public static void AlterNow2(AlterType type)
{
   if (mapping == null)
   {
      mapping = typeof(AlterType).GetEnumValues().Cast().Join
                (
                   typeof(MyEventHandler).GetMethods(BindingFlags.Static | BindingFlags.Public), 
                   a => a, 
                   a => Enum.Parse(typeof(AlterType), a.Name.Replace("Alter", "")), (t, e) => new
                   {
                       Type = t,
                       EventHandler = e
                   }).ToDictionary(a => a.Type, 
                      a => new Action( ()=> a.EventHandler.Invoke(null, null)));
       mapping[type]();
   }
}

上等解是怎么做?我的上等解会用Attribute,不过我不想过度复杂这个例子,如果连组态外部档,DI都带进来,那就是信仰解。

看出LINQ的理解度?这个例子应用了LINQ的Join延迟执行特色,在Join当下产生Join的右端式,同样技巧可以用在GroupBy跟OrderBy等情境。

延迟执行,代表着可在执行当下改变条件式

就这样,报告完毕,欢迎在粉丝页留言讨论或是指教,不知道在哪?(眼眶湿....)    https://www.facebook.com/cooldotnet/ 。

喔,最后你问这几ms的性能有差吗?你知道60 FPS每个 frame 只有16.6 ms可以用,你说有没有差呢?

然后你问我有没有可能我的程序有错?当然有可能啊。

我这辈子只有在Console印Hello World这种程序才有87% 自信自己没有错好吗?