欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

【C#夯实】我与接口二三事:IEnumerable、IQueryable 与 LINQ

程序员文章站 2023-10-17 09:06:12
序 学生时期,有过小组作业,当时分工一人做那么两三个页面,然而在前端差不多的时候,我和另一个同学发生了争执。当时用的是简单的三层架构(DLL、BLL、UI),我个人觉得各写各的吧,到时候合并,而他觉得应该把底层先写好,他好直接调用中间层的方法。 到出来工作之后,接触接口,想整理一下这个:接口到底是个 ......

  学生时期,有过小组作业,当时分工一人做那么两三个页面,然而在前端差不多的时候,我和另一个同学发生了争执。当时用的是简单的三层架构(dll、bll、ui),我个人觉得各写各的吧,到时候合并,而他觉得应该把底层先写好,他好直接调用中间层的方法。

  到出来工作之后,接触接口,想整理一下这个:接口到底是个什么概念呢?

  需要说明一点的是,我这里说的接口,不是api那个接口,而是“暂时没实现”那个接口。

  刚接触接口类型的时候,还不太熟练,看到返回接口类型的方法,总在奇怪,这个返回的对象怎么知道它取哪个实现?可以看一个简单的例子:

报错  

(无法创建抽象类或接口的实例  

    var test = new itestinterface();

正确   

    itestinterface infa = new testinterface();

    infa.func1();

  也即,返回的类型总是具类,是确定的,方法已经实现的

itestinterface infa = new testinterface();

   其中的 itestinterface 更像一个模具,对应这个模具造型的内容,由testinerface提供。

  那么,接口到底如何使用?

  接口的使用,要这样看:“具备某种特征(功能)”。

  例如看 itestinterface infa = new testinterface(); 其中,testinterface具备有itestinterface的特征,而itestinterface作为有某种特征(功能)的标记,它对具体如何达到这种特征(功能)是不感兴趣的,有标记就有特征。这种标记的体现,在c#里面就是继承。

  说到这里,老朋友ienumerable是一定要介绍的。

 

一、迭代器 ienumerable

  集合这种数据结构是很常见的,通常的操作是对集合的内容做筛选,或排序。ienumerable接口描述的是返回可循环访问集合的枚举数,继承这个接口,需要实现 public ienumerator getenumerator() {} 方法。

  那么,ienumerator是个什么er?继承这个接口之后,ide提示需要实现的方法——

    public class iterator : ienumerator
    {
        public object current => throw new notimplementedexception();
        public bool movenext()  { … }
        public void reset()  { … }
    }

   有一个当前对象,一个是否能指向下一个的判断,还有一个重置。那么,可以想象迭代器应该是这样用的:

    iterator iterator = new iterator();
    while (iterator.movenext())
    {
        // get iterator.current to do something..
        console.writeline(iterator.current.tostring());
    }

  但这看起来,并不太聪明,或者这样使用比较“合理”:

 【C#夯实】我与接口二三事:IEnumerable、IQueryable 与 LINQ

  是不是get到了某种真相?foreach里面接受的是ienumerable对象,并且会在此处调用到getenumerator去得到enumerator。那么到底public ienumerator getenumerator(){}要怎么实现呢,c# 2已经提供了yield语句简化迭代器。

    public class iterationsample : ienumerable
    {
        public ienumerator getenumerator()
        {
            for (int index = 0; index < values.length; index++)
            {
                yield return values[(index + startingpoint) % values.length];
            }
        }

        public object[] values;
        public int startingpoint;

        public iterationsample(object[] values, int startingpoint)
        {
            this.values = values;
            this.startingpoint = startingpoint;
        }
    }

   再来使用enumerator:

    object[] objs = new object[]{"a", "b", "c", "d"};
    iterationsample sam = new iterationsample(objs, 0);
    foreach (var str in sam)
    {
        // do something..
    }

  可以想象,yield是个怎么样的存在,“一次一次返回”这是我对yield的第一印象描述。但总觉得还是有些说不清楚,这种时候还是得看看书:

  “yield return 语句指表示 ’暂时地’ 退出方法——事实上,可以把它当做暂停”,

  既然有这种说法,那还得给出个demo[1],关于怎么个“暂停”。

  (这里悄咪咪用c# 6的新语法using static system.console; 实在懒得打 console.writeline();)

    class program
    {
        static void main(string[] args)
        {
            ienumerable<int> iterable = createenumerable();
            ienumerator<int> iterator = iterable.getenumerator();
            writeline("starting to iterate");
            while (true)
            {
                writeline("calling movenext()..");
                bool result = iterator.movenext();
                writeline($"movenext result = {result}");
                if (!result) break;
                writeline("fetching current..");
                writeline($"..current result = {iterator.current.tostring()}");
            }
            readline();
        }

        static readonly string padding = new string(' ', 30);
 
        static ienumerable<int> createenumerable()
        {
            writeline("start of createenumerable()");
            for (int i = 0; i < 2; i++)
            {
                writeline($"{padding} about to yield {i}");
                yield return i;
                writeline($"{padding} after yield");
            }
            writeline($"{padding} yielding final value");
            yield return -1;

            writeline($"{padding} end of createenumerable");
        }
    }

【C#夯实】我与接口二三事:IEnumerable、IQueryable 与 LINQ 

  此处可以留意“after yield”是什么时候出现的,就会发现[1]:

   l   在第一次调用movenext之前,createenumerable中的代码不会被调用;

   l   当调用movenext时,current也同时变化;

   l   在yield return的位置,代码就停止执行,在下一次调用movenext时又继续执行(再return一次)

  yield的故事还没有完,此处就简短介绍。

 

  yield return提供了逐个返回的条件,对于仅是取集合当中符合筛选条件的一项,用yield是方便的,逐个返回的情况下,不会占用过多的存储空间。但如果涉及到排序(或者比大小、最值)的问题,那必然要求集合当中的所有数据处于可用状态,这里也出现了一些传值的概念。

  yield return属于延迟执行(deferred execution),延迟执行再区分为惰性求值(lazy evaluation)和热情求值(eager evaluation)。 

deferred but eager execution

deferred and lazy execution

    ienumerable<int> getcomputation(int maxindex)

    {

        var result = new int[maxindex];

        for(int i = 0; i < maxindex; i++)

        {

            result[i] = computation(i);

        }

        foreach(var value in result)

        {

            yield return value;

        }

    }

    ienumerable<int> getcomputation(int maxindex)

    {

        for(int i = 0; i < maxindex; i++)

        {

            yield return computation(i);

        }

    }

  详见:

 

  下面这个例子,是惰性求值,迭代器返回的值受lambda表达式控制,并且是在每一次访问到这一个“点”的时候,再去返回 “点”的处理结果。热情求值是直接返回“点”,没有再过处理。两相比较,还得看具体的编程情况以作选择,此处不赘述。

    static void main(string[] args)
    {
        var sequence = generate(10, () => datetime.now);
        foreach (var value in sequence)
            writeline($"{value:t}");
    }

    static ienumerable<tresult> generate<tresult>(int number, func<tresult> generator)
    {
        for (var i = 0; i < number; i++)
        {
            sleep(400);
            yield return generator();
        }
    }

   (为了逻辑上的全面性,)与延迟执行相对的是立即执行(immediately execution),是一次返回就完成函数的操作。

 

二、迭代器 iqueryable

  linq to object 是针对本地数据存储(local data store)来执行查询的,系统会根据lambda表达式里面的逻辑创建匿名的委托,并执行代码;

  linq to sql 针对的是在数据库执行的,会把查询条件解析成t-sql,并且把sql语句发送给数据库引擎。

 

  关于,自动生成sql语句这一点,可以做个尝试,例如:创建了一个ef,调试监控连接数据库后返回的变量类型。

    var dbcontext = new cm_fortestentities();
    var tb1 = dbcontext.tblemployees;
    var tb2 = dbcontext.tblemployees.where(a => a.id == 1);
    var tb3 = dbcontext.tblemployees.where(a => a.gender == "male").orderbydescending(a => a.id);

 

【C#夯实】我与接口二三事:IEnumerable、IQueryable 与 LINQ 

 

  咋一看,怎么还能是不同类型?但是再看类成员,会发现一些端倪:

public abstract class dbset : dbquery, iinternalsetadapter
public abstract class dbquery : iorderedqueryable, iqueryable, ienumerable, ilistsource, iinternalqueryadapter

public interface iorderedqueryable : iqueryable, ienumerable

 

  好了,终于引入到这个朋友——iqueryable,iqueryable有些什么必要实现的方法呢?

    public class queryablesample : iqueryable
    {
        public expression expression => throw new notimplementedexception();
        public type elementtype => throw new notimplementedexception();
        public iqueryprovider provider => throw new notimplementedexception();
        public ienumerator getenumerator()
        {  throw new notimplementedexception(); }
    }

 

  iqueryable是ienumerable的孩子(iqueryable : ienumerable),它是一个有自己花样的迭代器。这个花样如何体现呢?关键还在于expression、iqueryprovider上。

  从字面上来看,expression是查询条件的表达式树;那么provider就是提供数据的成员了。

    public class queryablesample : iqueryable
    {
        public expression expression { get; }
        public type elementtype => typeof(modelitem);
        public iqueryprovider provider { get; }

        public ienumerator getenumerator()
        {
            return provider.execute<ienumerable>(expression).getenumerator();
        }

        ienumerator ienumerable.getenumerator()
        {
            return getenumerator();
        }

        public queryablesample(iqueryprovider provider, expression expression)
        {
            if (provider == null)
                throw new argumentnullexception("provider");
            if (expression == null)
                throw new argumentnullexception("expression");

            provider = provider;
            expression = expression;
        }
    }

  预感中,provider会是个重要角色:

public class queryprovider : iqueryprovider

iqueryable createquery(expression expression)

return new queryablesample(this, expression);

iqueryable<telement> createquery<telement>(expression expression)

return (iqueryable<telement>) new queryablesample(this, expression);

object execute(expression expression)

return queryresult.execute(expression, false);

tresult execute<tresult>(expression expression)

bool isenumerable = (typeof(tresult).name == "ienumerable`1");

return (tresult)queryresult.execute(expression, isenumerable);

 

    public class queryprovider : iqueryprovider
    {
        public iqueryable createquery(expression expression)
        {
            return new queryablesample(this, expression);
        }
        public iqueryable<telement> createquery<telement>(expression expression)
        {
            return (iqueryable<telement>) new queryablesample(this, expression);
        }
        public object execute(expression expression)
        {
            return queryresult.execute(expression, false);
        }
        public tresult execute<tresult>(expression expression)
        {
            bool isenumerable = (typeof(tresult).name == "ienumerable`1");
            return (tresult)queryresult.execute(expression, isenumerable);
        }
    }
    public sealed class queryresult
    {
        public static object execute(expression expression, bool isenumerable)
        { // 利用expression得到数据结果,设其为records
            queryablesample records = null;
            if (isenumerable)
                return records.provider.createquery(expression);
            else
                return records.provider.execute(expression);
        }
    }

 【C#夯实】我与接口二三事:IEnumerable、IQueryable 与 LINQ

  在github上找到了个详尽些的queryabledemo可以看: https://github.com/andreychizhov/nestqueryableprovider

 

三、ienumerable 与 iqueryable

   下面以一个例子比较二者最大的区别[2]:

            var q = from c in dbcontext.customers

                       where c.city == "london"

                       select c;

            var finalanswer = from c in q

                                      orderby c.name

                                      select c;

 

使用iqueryable<t>所内置的linq to sql机制。

(linq to sql程序库会把相关的查询操作合起来执行,仅向数据库发出一次调用,即where和orderby都是在同一次sql查询中完成。)

            var q = (from c in dbcontext.customers

                        where c.city == "london"

                        select c).asenumerable();

            var finalanswer = from c in q

                                      orderby c.name

                                      select c;

 

把数据库对象强制转换成ienumerable形式的序列,并把排序等工作放在本地完成。

(即会把where字句后得到的结果转换成ienumerable<t>的序列,再采用linq to objects机制完成后续,排序是通过委托在本地执行。)

  注意:

  两种不同的数据处理方式,依循着两套完全不同的流程。无论是用lambda表达式来撰写查询逻辑还是以函数参数的形式来表示这些逻辑,针对ienumerable<t>所设计的那些扩展方法都将其视为委托。反之,针对iqueryable<t>的那些扩展方法用的则是表达式树。【表达式树 可以把各种逻辑合并起来成一条sql语句。】

 

public static ienumerable<tsource> where<tsource>(this ienumerable<tsource> source, func<tsource, bool> predicate)
public static iqueryable<tsource> where<tsource>(this iqueryable<tsource> source, expression<func<tsource, bool>> predicate)

 

  如果使用ienumerable<t>,则必须在本地进行。系统把lambda表达式编译到方法里,在本地计算机上运行,这意味着无论有待处理的数据在不在本地,都必须先获取过来才行。

  同时,用来支持iqueryable的那些provider未必能够完全解析每一种查询,通常这些provider只能解读几种固定的(.net framework已经实现)的运算符(方法),如果要在查询操作里面调用除此之外的其它方法,那可能就得把序列当成ienumerable来查询。

 

吐槽    :emmmmmm,,,本来是想写我与接口二三事,结果竟然如此跑偏,太多细节能扣啦,知识点冥冥间也有关联,慢慢捋吧~

立flag:本月开启机器学习,今年要把c#基础篇搞定。

 

注释:

[1] 自《深入理解c#》(第3版)jon skeet 著  姚琪琳 译

[2] 自《effective c#》(第3版) 比尔·瓦格纳 著