• Linq研究


    微软在.NET 3.5中加入了LINQ技术,作为配套改进了C#语言,加入了Lambda表达式,扩展方法,匿名类型等新特性用以支持LINQ。微软同时提出了要使用声明式编程,即描述计算规则,而不是描述计算过程。

    使用LINQ技术能很好地做到声明式编程,写出的代码表意能力强,可读性高,避免了以往或其他语言的代码中充斥大量表意不明的for循环甚至多层循环的问题。不要小看for循环和Where,Select,OrderBy等扩展方法的区别,可以不通过注释一眼就能看出代码意图真的很重要。当看到Java代码中一大堆的for循环,包括多层循环,又没有注释,必须仔细看才能了解代码作用时,真的很头大。个人认为LINQ是C#语言区别于其他语言的最显著的特性,也是最大的优势之一。

    当然现在大多数主流语言都加入了Lambda表达式,从而可以使用类似于LINQ的技术,达到声明式编程。比如Java语言在Java 8中加入了和C#几乎一样的Lambda表达式语法,并加入了Stream API,以达到类似于LINQ的用法。

    如此可见,声明式编程是发展趋势,既然使用C#,就要多用LINQ,用好LINQ,用对LINQ。不要再写一堆一堆的for循环了!

    要用好LINQ,就要学好LINQ,理解其原理,机制和用法。推荐一个学习和研究LINQ的好工具LINQPad,下面是官网和官网上的截图。

    http://www.linqpad.net/

    Image

    下面针对几个关键点,对LINQ进行一些初步研究。有些问题可能是使用LINQ多年的人都理解得不对的。

    首先看下面的程序。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    namespace LinqResearch
    {
        class Program
        {
            static void Main(string[] args)
            {
                var list = new List<int> { 2, 1, 6, 4, 3, 5, 7, 8, 10, 9 };
                Console.WriteLine("list");
                var list1 = list.Select(i =>
                {
                    Console.WriteLine("In select {0}", i);
                    return i * i;
                });
                Console.WriteLine("list1");
                var list2 = list1.Where(i =>
                {
                    Console.WriteLine("In where>10 {0}", i);
                    return i > 10;
                });
                Console.WriteLine("list2");
                var list3 = list2.Where(i =>
                {
                    Console.WriteLine("In where<60 {0}", i);
                    return i < 60;
                });
                Console.WriteLine("list3");
                var list4 = list3.OrderBy(i =>
                {
                    Console.WriteLine("In orderby {0}", i);
                    return i;
                });
                Console.WriteLine("list4");
                var list5 = list4.ToList();
                Console.WriteLine("list5");
                foreach (var i in list5)
                {
                    Console.WriteLine(i);
                }
            }
        }
    }

    先不要看下面的运行结果,想想打印出的是什么,然后再看结果,看看和想的一样吗?

    list
    list1
    list2
    list3
    list4
    In select 2
    In where>10 4
    In select 1
    In where>10 1
    In select 6
    In where>10 36
    In where<60 36
    In select 4
    In where>10 16
    In where<60 16
    In select 3
    In where>10 9
    In select 5
    In where>10 25
    In where<60 25
    In select 7
    In where>10 49
    In where<60 49
    In select 8
    In where>10 64
    In where<60 64
    In select 10
    In where>10 100
    In where<60 100
    In select 9
    In where>10 81
    In where<60 81
    In orderby 36
    In orderby 16
    In orderby 25
    In orderby 49
    list5
    16
    25
    36
    49

    为什么先打印出list 到 list4,而没有进到Lambda里面?

    这是因为LINQ是延时计算的,即只有foreach或ToList时才去做真正计算,前面的Select,Where等语句只是声明了计算规则,而不进行计算。

    这点很重要,如果不明白这点,就会写出有BUG的代码,如下面的程序,打印出的是1和2,而不是1。

                var a = 2;
                var list = new List<int> { 1, 2, 3 };
                var list1 = list.Where(i => i < a);
                a = 3;
                foreach (var i in list1)
                {
                    Console.WriteLine(i);
                }

    后面打印出的为什么先是select和where交错,然后是orderby,而不是先select再where,最后orderby?

    这时因为Select,Where等这些扩展方法,在声明计算规则时是有优化的(内部可能通过表达式树等方法实现),它并不是傻傻的按照原始定义的规则,顺序执行,而是以一种优化的方法计算并获得结果。所以使用 LINQ一般会比自己写的原始的一大堆for循环性能还高,除非花大量时间优化自己的逻辑(一般不会有这个时间)。

    可以看到针对元素2和1,并没有打印出In where<60 的行,这说明针对这两个元素,第二个Where里的代码并没有执行,因为第一个Where都没有通过。在进行完投影(Select)和筛选(Where)后,最后进行排序(OrderBy),只针对筛选后留下的元素执行OrderBy里面的计算逻辑,一点也不浪费。

    上面的程序有人可能会写成这样。

                var list = new List<int> { 2, 1, 6, 4, 3, 5, 7, 8, 10, 9 };
                Console.WriteLine("list");
                var list1 = list.Select(i =>
                {
                    Console.WriteLine("In select {0}", i);
                    return i * i;
                }).ToList();
                Console.WriteLine("list1");
                var list2 = list1.Where(i =>
                {
                    Console.WriteLine("In where>10 {0}", i);
                    return i > 10;
                }).ToList();
                Console.WriteLine("list2");
                var list3 = list2.Where(i =>
                {
                    Console.WriteLine("In where<60 {0}", i);
                    return i < 60;
                }).ToList();
                Console.WriteLine("list3");
                var list4 = list3.OrderBy(i =>
                {
                    Console.WriteLine("In orderby {0}", i);
                    return i;
                }).ToList();
                Console.WriteLine("list4");
                var list5 = list4.ToList();
                Console.WriteLine("list5");
                foreach (var i in list5)
                {
                    Console.WriteLine(i);
                }

    这样写打印出的结果为,

    list
    In select 2
    In select 1
    In select 6
    In select 4
    In select 3
    In select 5
    In select 7
    In select 8
    In select 10
    In select 9
    list1
    In where>10 4
    In where>10 1
    In where>10 36
    In where>10 16
    In where>10 9
    In where>10 25
    In where>10 49
    In where>10 64
    In where>10 100
    In where>10 81
    list2
    In where<60 36
    In where<60 16
    In where<60 25
    In where<60 49
    In where<60 64
    In where<60 100
    In where<60 81
    list3
    In orderby 36
    In orderby 16
    In orderby 25
    In orderby 49
    list4
    list5
    16
    25
    36
    49

    虽然也能得到正确的结果,但是却是不合理的。因为这样写每步都执行计算,并放到集合中,会有很大的性能损耗,失去了使用LINQ的优势。

    何时进行真正计算是个值得思考的问题,多了会增加中间集合的数量,性能不好,少了有可能会有多次重复计算,性能也不好。下文会有说明。

    如果使用Resharper插件,会提示出重复迭代(可能会有多次重复计算)的地方,这个功能很好,便于大家分析是否存在问题。

    使用Max和Min要小心,Max和Min等聚合运算需要集合中存在值,否则会抛出异常,笔者多次遇到这个问题产生的BUG。

    当前面有Where筛选时,后面使用Max或Min不一定是安全的,如下面的代码会抛出异常。

                var a = 0;
                var list = new List<int> { 1, 2, 3 };
                var min = list.Where(i => i < a).Min();
                Console.WriteLine(min);

    如果a来源于外部值,又有大段的逻辑,这样的BUG不易发现。

    解决方法有多种,我们来分析一下,一种方法是可以先调一下Any,再使用Min,代码如下,

                var a = 0;
                var list = new List<int> { 1, 2, 3 };
                var list2 = list.Where(i => i < a);
                var min = 0;
                if (list2.Any())
                {
                    min = list2.Min();
                }
                Console.WriteLine(min);

    把代码改为如下,

                var a = 3;
                var list = new List<int> { 1, 2, 3 };
                var list2 = list.Where(i =>
                {
                    Console.WriteLine("In where {0}", i);
                    return i < a;
                });
                var min = 0;
                if (list2.Any(i =>
                {
                    Console.WriteLine("In any {0}", i);
                    return true;
                }))
                {
                    min = list2.Min();
                }
                Console.WriteLine(min);

    打印结果为,

    In where 1
    In any 1
    In where 1
    In where 2
    In where 3
    1

    这样做有可能对性能影响不大,也有可能较大,取决于where(或前面的其他逻辑)中逻辑的多少和集合中前面不满足where条件的元素的数量。因为Any确定有就不会继续执行,但仍有部分重复计算发生。

    第二种方法的代码如下,

                var a = 3;
                var list = new List<int> { 1, 2, 3 };
                var list2 = list.Where(i => i < a).ToList();
                var min = 0;
                if (list2.Any())
                {
                    min = list2.Min();
                }
                Console.WriteLine(min);

    这种方法不会有重复计算的开销,但会有数据导入集合的开销,和第一种比较哪种性能更高值得考虑。

    第三种方法的代码如下,

                var a = 0;
                var list = new List<int> { 1, 2, 3 };
                var list2 = list.Where(i => i < a);
                var min = 0;
                try
                {
                    min = list2.Min();
                }
                catch (Exception)
                {
                }
                Console.WriteLine(min);

    直接吃掉异常,数据量大时,前面过滤条件计算复杂时,可能这种方法性能最高。

    总之,C#开发者,学好LINQ,用好LINQ,你会发现真的很爽的!

  • 相关阅读:
    [Java解惑]数值表达式
    Java使用LdAP获取AD域用户
    LDAP Error Codes
    Excel向上取整
    java中的三种取整函数
    Dwz手册的补充说明和常见问题
    【转】BSON数据格式
    go语言合并两个数组
    vscode远程修改文件('file': A system error occured )
    [转]Linux 桌面玩家指南:20. 把 Linux 系统装入 U 盘打包带走
  • 原文地址:https://www.cnblogs.com/clockdotnet/p/4188328.html
Copyright © 2020-2023  润新知