• C#复习笔记(4)--C#3:革新写代码的方式(查询表达式和LINQ to object(上))


    查询表达式和LINQ to object(上)

    LINQ中的概念介绍

    序列

    序列是LINQ的基础,它也是数据处理流模型的基础,让我们能只在需要的时候才对数据进行获取和处理。

    在C#中的序列通过IEnumerable 和 IEnumerable<T> 接口进行封装,序列就像数据项的传送带——你每次只能获取它们一个, 直到你不再想获取数据, 或者序列中没有数据了。

    序列与其他的数据集合结构相比最大的区别在于,你通常不知道序列有多少项构成--或者不能访问任意项,只能是当前这个。列表和数组也能作为序列, 因为 List< T> 实现了IEnumerable< T>—— 不过, 反过来并不总是可行。 比如,你不能拥有一个无限的数组或列表。

    不管时流式传输还是缓冲式的传输,他们都属于延迟操作,就是只有在枚举结果集中的第一个元素时才会真正的传输数据,与此相对的是立即执行——有一些转换一经调用就会立即执行。一般来说,返回另一个序列的操作(通常是IEnumerable<T>和IQueryable<T>)会进行延迟操作,返回单一值的运算会立即执行。

    查询表达式

    首先看原理,下面定义了一个类Dummy,有一个实例方法Select,还给它定义了一个扩展方法Where:

    class Dummy<T>
        {
            public Dummy<T> Select<T>(Func<T, bool> predicate)
            {
                Console.WriteLine("Select called");
                return new Dummy<T>();
            }
        }
    
        static class Extenssion
        {
            public static Dummy<T> Where<T>(this Dummy<T> dummy, Func<T, bool> predicate)
            {
                Console.WriteLine("Where called");
                return dummy;
            }
        }

    static void Main(string[] args)
    {
    var source = new Dummy<string>();
    var query = from dummy in source
    where dummy.ToString() == "Ignored"
    select "Anything";

    Console.ReadKey();
    }

    C#编译器在看到上面这段main方法是会进行一种机械式的转译,这种转译也叫做鸭子类型式的转译(类似的还有async/await模式中的可等待模式,只要实现了一个GetAwaiter就行,还有foreach语句中,只要实现了GetEnumerator就行,扯的有点儿远),他会首先查看Dummy类型上面能不能够找到对应的方法,按上面的代码来说,当看到where(注意是小写)时,会查看Dummy类是否有Where实例方法或者扩展方法存在,当看到select(注意是小写)时,采用同样的方式来转译。最终会称为下面的样子:

    var query = source.Where(dummy => dummy.ToString() == "Ignored").Select(dummy => "anything");

    select子句是一个投影,能够将范围变量(下面会讲)的类型转换为其他类型或者原样输出。需要注意的是当以下几种情况发生时select语句会被优化掉:

    //当满足下面列出这两种情况时(同时满足),select会被编译器删除掉。
    var query = from
    person in persons where person.Age>18 //①当有其他语句执行时 select person; //②select的投影返回的类型与范围类型相同,相当于select什么都没做。

    范围变量

    上面代码中的user就是一个范围变量声明。范围变量不像其他种类的变量。在某些方面,它根本就不是变量。 它们只能用于查询表达式中, 实际代表了从一个表达式传递给另外一个表达式的上下文信息。 它们表示了特定序列中的一个元素,而且它们被用于编译器转译中,以便把其他表达式轻易地转译为Lambda表达式。

    我们已经知道最初的查询表达式会转换为如下形式:

    SampleData.AllUsers.Select(user => user)

    lambda表达式的左边,就是范围变量,而右边,就是select子句的逻辑,转译的过程就是这么简单。

    Cast和OfType操作符

    到目前为止,我们都实在一个强类型的集合上面使用查询操作符,但是,还有一些弱类型的集合,比如ArrayList和object[],这个时候,Cast和OfType操作符就排上用场了。

    static void Main(string[] args)
            {
    
                ArrayList list = new ArrayList() { "first", "second", "third" };
                IEnumerable<string> strings = list.Cast<string>();
                foreach (string item in strings)
                {
                    Console.WriteLine(item);//依次输出"first","second","third"
                }
                ArrayList anotherList = new ArrayList()
               {
                   1,"first",3,"fourth"
               };
                IEnumerable<int> ints = anotherList.OfType<int>();
                foreach (int item in ints)
                {
                    Console.WriteLine(item);//依次输出1,3
                }
                Console.ReadKey();
            }

    在将这种弱类型的集合转换成强类型的集合时,Cast和OfType的机制有所不同,Case会尝试转换每一个元素,遇到不支持的类型时,就会报错,但注意报错的时机:只有在输出1之后,才进行报错,因为Cast和OfType都对序列进行流处理。而OfType会尝试去转换每一个元素,跳过那些不合格的元素。

    Cast和OfType只允许一致性、拆箱和装箱转换。List<int>和List<short>之间的转换会失败——Cast会报异常,OfType不会。

    而在查询表达式中,显式的声明范围变量的类型和Cast的执行绑定到了一起:如果在一个弱类型的集合中显示的声明范围变量的类型:

    static void Main(string[] args)
            {
    
                ArrayList list = new ArrayList() { "first", "second", "third" };
                var query = from string oneString in list
                    select oneString.Substring(0, 3);
                foreach (string item in query)
                {
                    Console.WriteLine(item);
                }
                      Console.ReadKey();
            }

    这个被转译后就会编程这样

      var anotherQuery = list.Cast<string>().Select(li => li.Substring(0, 3));

    没有这个类型转换(Cast)我们根本就不能调用Select————因为Select是只能用于IEnumerable<T>而不能用于IEnumerable。也就是说,Cast和OfType这两个操作符能将一个弱类型的集合变成一个强类型的集合。

    当然,除了在弱类型的集合中使用显式声明的范围类型变量,在强类型中也会这样使用。比如,List<接口>中你可能想使用显式类型为”接口实现“声明的范围类型,因为你知道这个List中装的都是”接口实现“而不是”接口“。

    接下来阐述一些重要的概念:

    • LINQ以数据序列为基础, 在任何可能的地方都进行流处理。
    • 创建一个查询并不会立即执行它:大部分操作都会延迟执行。
    • C#3的查询表达式包括一个把表达式转换为普通C#代码的预处理阶段,接着使用类型推断、重载、Lambda表达式等这些常规的规则来恰当地对转换后的代码进行编译。
    • 在查询表达式中声明的变量的作用:它们仅仅是范围变量,通过它们你可以在查询表达式内部一致地引用数据。

    透明操作符

    对于透明操作符的解释可以用let来说明:let关键字可以在一个查询表达式中额外的声明一个范围变量,这个范围变量和查询表达式中的某一个变量有联系。

    let的用途可以用一下代码来说明:

     var query6 = from user in SampleData.AllUsers
                             orderby user.Name.Length
                             select user.Name;
                foreach (string name in query6)
                {
                    Console.WriteLine($"{name}:{name.Length}");
                }

    上面代码的意图是取出用户的名字并将改名字按照名字的长度排序。我们可以看到这段代码调用了Name属性Length两次,这是一种严重的性能问题,可以避免,使用let来看看:

    var query6 = from user in SampleData.AllUsers
                             let length = user.Name.Length
                             orderby length
                             select new { user.Name, length };
                foreach (var item in query6)
                {
                    Console.WriteLine($"{item.Name}:{item.length}");
                }

    下面是转译后的代码:

    var query6 = SampleData.Users .Select(user => new {user, length = user.Name.Length}) 
    .OrderBy(z
    => z.length)
    .Select(z
    => new {Name = z.user.Name, Length = z.length});

    使用let声明了一个length的范围变量,至此,在这个查询表达式中我们拥有了两个范围变量,但是我们知道,在转移后的Select方法中包含的lambda表达式只会传入一个变量,所以,这里引入了一个透明标识符的概念:编译器给你创建了一个匿名类型(上面转译后代码中的第一个Select),这个匿名类型包含了上面创建的两个范围变量。

  • 相关阅读:
    keras与卷积神经网络(CNN)实现识别mnist手写数字
    Pytorch自动求解梯度
    Kaggle竞赛入门(四):随机森林算法的Python实现
    Kaggle竞赛入门(三):用Python处理过拟合和欠拟合,得到最佳模型
    Kaggle竞赛入门(二):如何验证机器学习模型
    Kaggle竞赛入门(一):决策树算法的Python实现
    R语言入门:正态分布中dnorm(),pnorm(),qnorm(),和rnorm()函数的使用
    初识suse-Linux相关!
    关于visio 2007导入独立图库
    让windows 2003启动后直接进入桌面
  • 原文地址:https://www.cnblogs.com/pangjianxin/p/8682573.html
Copyright © 2020-2023  润新知