• .NET面试题系列[12]


    "为了使LINQ能够正常工作,代码必须简化到它要求的程度。" - Jon Skeet

    为了提高园子中诸位兄弟的英语水平,我将重要的术语后面配备了对应的英文。

    .NET面试题系列目录

    隐式类型的局部变量

    隐式类型允许你用var修饰类型。用var修饰只是编译器方便我们进行编码,类型本身仍然是强类型的,所以当编译器无法推断出类型时(例如你初始化一个变量却没有为其赋值,或赋予null,此时就无法推断它的类型),用var修饰就会发生错误。另外,只能对局部变量使用隐式类型。

    使用隐式类型的几个时机:

    1. 当变量的类型太长或者难以推测,但类型本身不重要时,比如你的LINQ语句中用了Groupby,那么一般来说基本很少人可以准确地推测出结果的类型吧。。。
    2. 当变量初始化时,此时可以根据new后面的类型得知变量类型,故不会对可读性造成影响
    3. 在Foreach循环中你迭代的对象,此时一般不需要显式指出类型

    总的来说,如果使用隐式类型导致你的代码的可读性下降了,那么就改用显式类型。一般第二条原则已经是一个不成文的规定了。Resharper在检测到变量初始化时,如果你没有使用隐式类型,也会提醒你可以用var代替之。

    LINQ中隐式类型的体现:你可以统统用var来修饰LINQ语句返回的类型。一般来说LINQ语句的返回类型通常名字都比较长,而且也不是十分显而易见。如果没有隐式类型,在写代码时就会比较痛苦。

    自动实现的属性

    现在应该满世界都在用自动实现的属性了。注意在结构体中使用自动实现的属性(注意字段不需要),需要显式的调用无参构造函数this()。这是结构体和类的一个区别。

    public struct Foo
        {
            public int a { get; private set; }
    
            Foo(int A) : this()
            {
                a = A;
            }
        }

    上面代码如果去掉this()将会发生错误,在默认无参构造函数将结构体的属性设为默认值之前,不能使用这些属性。如果将上面代码的属性改为字段,则即使不调用this()也不会有问题。

    匿名类型(Anonymous Type) 

    匿名类型允许你直接在括号中建立一个类型。虽然不需要指定成员的具体类型,但匿名类型的成员都是强类型的。

            static void Main(string[] args)
            {
                var tom = new {Name = "Tom", Age = 15};
                Console.WriteLine("{0}: {1}", tom.Name, tom.Age);
            }

    对匿名类型进行初始化之后,就可以如同实际类型一样使用点符号获取匿名类型的成员,但变量tom只能用var或者object修饰。如果两个匿名类型有相同数量的成员,且所有成员拥有相同的类型名称和值的类型,而且以相同的顺序出现,则编译器会将它们看作是同一个类型。

            static void Main(string[] args)
            {
                var family = new[]
                {
                    new {Name = "Tom", Age = 15},
                    new {Name = "Jerry", Age = 16}
                };
                var cat = new {Age = 27, Name = "Cat"};
                var dog = new {Age = 2222222222222222, Name = "Dog"};
    
            }

    如果在初始化中交换了属性的顺序,或者某个属性使用了long而不是int,则会引入一个新的匿名类型。

    匿名类型包含了一个默认的构造函数,它获取你赋予的所有初始值。另外,它包含了你定义的类型成员,以及继承自object类型的若干方法(重写的Equals, 重写的GetHashCode, ToString等等)。同一个匿名类型的两个实例在判断相等性时,采用的是依次比较每个成员的值的方式。

    在LINQ中,我们可以使用匿名类型来装载查询返回的数据,尤其是最后使用Select或SelectMany等方法返回若干列时。在每次查询都要为返回数据定制一个类显得太繁琐了,虽然有时候是需要的(ViewModel),但也有时候只是为了一次性的展示数据。如果你要创建的类型只在一个方法中使用,而且其中只有简单的字段或者属性而没有方法,则可以考虑使用匿名类型。

    表达式和表达式树(Expression & Expression Tree)

    Express是表达的意思(它还有很多其他意思,例如快速的),加上名词后缀-sion即为表达式。

    表达式是当今编程语言中最重要的组成成分。简单的说,表达式就是变量、数值、运算符、函数组合起来,表示一定意义的式子。例如下面这些都是(C#的)表达式:

    • 3                             //常数表达式
    • a                             //变量或参数表达式
    • !a                            //一元逻辑非表达式
    • a + b                       //二元加法表达式
    • Math.Sin(a)              //方法调用(lambda)表达式
    • new StringBuilder()   //new 表达式

    表达式的一个重要的特点是它可以无限组合,只要符合正确的类型和语义。表达式树则是将表达式转换为树形结构,其中每个节点都是表达式。表达式树通常被用于转换为其他形式的代码。例如LINQ to SQL将表达式树转译为SQL。

    最基本的几种表达式

    • 常量表达式:Expression.Constant(常量的值);
    • 变量表达式:Expression.Parameter(typeof(变量类型), "变量名称")
    • 二元表达式,即需要两个表达式作为参数进行操作的表达式:Expression.[某个二元表达式的方法,例如加减乘除,模运算等](表达式1, 表达式2);
    • Lambda表达式:表达一个方法,可以接受一个代码段或一个方法调用表达式作为方法,以及一组方法参数。Lambda为一希腊字母,无法翻译。希腊字母还有很多,例如阿尔法,贝塔等。之所以选择这个字母是因为来自数学上的原因(数学上有lambda运算)

    构建一个最简单的表达式树1+2+3

    表达式树是对象构成的树,其中每个节点都是表达式。可以说,每个表达式都是一个表达式树,特别的,某些表达式可以看成只有一个节点的表达式树,例如常量表达式。System.Linq.Expressions命名空间下的Expression类和它的诸多子类就是这一数据结构的实现。Expression类是一个抽象类,主要包含一些静态工厂方法。Expression类也包含两个属性:

    • Type:代表表达式求值之后的.net类型,例如Expression.Constant(1)和Expression.Add(Expression.Constant(1), Expression.Constant(2))的类型都是Int32。
    • NodeType:代表表达式的种类。例如Expression.Constant(1)的种类是Constant,Expression.Add(Expression.Constant(1), Expression.Constant(2))的种类是Add。

    每个表达式都可以表示成Expression某个子类的实例。例如BinaryExpression就表示各种二元运算符(例如加减乘除)的表达式。它需要两个运算数(注意运算数也是表达式):

        public static BinaryExpression Add(Expression left, Expression right);

    Expression各个子类的构造函数都是不公开的,要创建表达式树只能使用Expression类提供的静态方法。

    要创建一个表达式树,首先我们要画出这个树,并找出它需要什么类型的表达式。例如如果我们要创建1 + 2 + 3这个表达式的表达式树,因为它太简单而且不包含多于一种运算(如果有加有乘还要考虑优先级),我们可以一眼看出,其只需要两种表达式,常量表达式(形容1,2,3)和二元表达式(形容加法),所以可以这样写:

    ConstantExpression exp1 = Expression.Constant(1);
    ConstantExpression exp2 = Expression.Constant(2);
    BinaryExpression exp12 = Expression.Add(exp1, exp2);
    ConstantExpression exp3 = Expression.Constant(3);
    BinaryExpression exp123 = Expression.Add(exp12, exp3);

    这个应该非常好理解。但如果我们想写出Math.Sin(a)这个表达式的表达式树怎么办呢?为了解决这个问题,Lambda表达式登场了,它可以表示一个方法。

    使用Lambda表达式表示一个函数

    我们的目标是使用Lambda表达式表示Math.Sin(a)这个表达式。Lambda表达式代表一个函数,现在它具有一个输入a(我们使用变量表达式ParameterExpression来代表,它应该是double类型),以及一个方法调用,这需要MethodCallExpression类型的表达式,方法名为Sin,位于Math类中。我们需要使用反射找出这个方法。

    代码如下:

    ParameterExpression expA = Expression.Parameter(typeof(double), "a"); //参数a
    MethodCallExpression expCall = Expression.Call(typeof(Math).GetMethod("Sin", BindingFlags.Static |   BindingFlags.Public), expA); //Math.Sin(a)
    LambdaExpression exp = Expression.Lambda(expCall, expA); // a => Math.Sin(a)

    使用Lambda表达式:通过Expression<TDelegate>

    Expression<TDelegate>泛型类继承了LambdaExpression类型,它的构造函数接受一个Lambda表达式。此处TDelegate指泛型委托,它可以是Func或者Action。泛型类以静态的方式确定了返回类型和参数的类型。

    对于上个例子,我们的输入和输出均为一个Double类型,故我们需要的委托类型是Func<double, double>:

    Expression<Func<double, double>> exp2 = d => Math.Sin(d);

    可以使用Compile方法将Expression<TDelegate>编译成TDelegate类型(在这个例子中,编译之后的对象类型为Func<double,double>),这是一个将表达式树编译为委托的简便方法(不需要再一步一步来,并且使用反射了)。编译器自动实现转换。

    然后就可以直接调用,获得表达式计算的结果:

    Expression<Func<double, double>> exp2 = d => Math.Sin(d);
    Func<double, double> func = exp2.Compile();
    Console.WriteLine(func(0.5));

    练习:使用两种方法构建表达式树(a, b, m, n) => m * a * a +  n * b * b

    假定所有的变量类型都是double。

    代码法:

    //(a, b, m, n) => m * a * a +  n * b * b
    ParameterExpression expA = Expression.Parameter(typeof(double), "a"); //参数a
    ParameterExpression expB = Expression.Parameter(typeof(double), "b"); //参数b
    ParameterExpression expM = Expression.Parameter(typeof(double), "m"); //参数m
    ParameterExpression expN = Expression.Parameter(typeof(double), "n"); //参数n
    
    BinaryExpression multiply1 = Expression.Multiply(expM, expA);
    BinaryExpression multiply2 = Expression.Multiply(multiply1, expA);
    BinaryExpression multiply3 = Expression.Multiply(expN, expB);
    BinaryExpression multiply4 = Expression.Multiply(multiply3, expB);
    BinaryExpression add = Expression.Add(multiply2, multiply4);

    委托法:

    Expression<Func<double, double, double, double, double>> exp4 = (a, b, m, n) => m*a*a + n*b*b;
    
    var ret = exp4.Compile();
    Console.WriteLine(ret.Invoke(1, 2, 3, 4)); // =3*1*1+4*2*2=3+16=19

    通过Expression<TDelegate>以及Compile方法,我们可以方便的计算表达式的结果。但如果一步步来,我们还需要手动遍历这棵树。既然使用代码构造表达式如此麻烦,为什么还要这样做呢?只是因为在手动遍历和计算表达式结果时,可以插入其他操作。LINQ to SQL就是通过递归遍历表达式树,将LINQ语句转换为SQL查询的,这是委托所不能替代的。

    不是所有的Lambda表达式都能转化成表达式树。不能将带有一个代码块的Lambda转化成表达式树。表达式中还不能有赋值操作,因为在表达式树中表示不了这种操作。

    参考资料:表达式树上手指南 http://www.cnblogs.com/Ninputer/archive/2009/08/28/expression_tree1.html

    扩展方法(Extension Method)

    扩展方法可以理解成,为现有的类型(现有类型可以为自定义的类型和.Net 类库中的类型)扩展(添加)一些功能,附加到该类型中。

    当我们要扩展某个类的功能时,有以下几种方法:一是直接修改类的代码,这可能会导致向后兼容的破坏(不符合开闭原则)。一是派生子类,但这增加了维护的工作量,而且对于结构和密封类根本不能这么做。扩展方法允许我们在不创建子类,不更改类型本身的情况下,仍然可以修改类型。

    扩展方法必须定义于静态的类型中,且所有的扩展方法必须是静态的。还是那句话,当你了解了类型对象时,你就很自然的理解了为何扩展方法必须是静态的。(它自类型对象被创建时就应当在对象的方法表中)

    扩展方法的第一个输入参数要加上this(第一个参数的类型表示被扩展的类型)。扩展方法必须至少要有一个输入参数。

    被扩展的类型的所有子类自动获得该扩展方法。

    当你的工程内有特定逻辑,且其基于一个比较普遍的类时,考虑使用扩展方法。如果你想为类型添加一些成员,但又不能更改类型本身(因为不属于你)时,考虑使用扩展方法。例如当你需要频繁判断字符串是否为Email时,你可以扩展String类,将这个判断方法单独置于一个叫做StringExtension的类型中,方便管理。之后你就可以通过调用String.IsEmail来方便的使用这个方法了。

    C#中提供了两个特别醒目的类:Enumerable和Queryable。两者都在System.Linq命名空间中。在这两个类中,含有许许多多的扩展方法。Enumerable的大多数扩展的是IEnumerable<T>,Queryable的大多数扩展的是IQueryable<T>。它们赋予了集合强大的查询能力,共同构成了LINQ的重要基础。

    什么是闭包(Closure)?C#如何实现一个闭包?

    闭包是一种语言特性,它指的是某个函数获取到在其作用域外部的变量,并可以与之互动。Closure这个单词显然来自动词close,有点动词名词化的意思。

    通过匿名函数或者lambda表达式,我们可以实现一个简单的闭包:

    static void Main(string[] args)
            {
                //外部变量
                var i = 0;
                //lambda表达式捕获外部变量
                //在外部变量的作用域内声明了一个方法
                MethodInvoker m = () =>
                {
                    //使用外部变量
                    i = i + 1;
                };
    
                m.Invoke();
                //打印出1
                Console.WriteLine(i);
            }

    此处函数和来自外部的变量i进行了互动。

    匿名函数(Anonymous Function)

    匿名函数出现于C# 2.0,它允许在一个委托实例的创建位置内联地指定其操作。

    例如我们可以这样写:

                Compare(c1, c2, delegate(Circle a, Circle b)
                {
                    if (a.Radius > b.Radius) return 1;
                    if (a.Radius < b.Radius) return -1;
                    return 0;
                });

    匿名方法的语法:先是一个delegate关键字,再是参数(如果有的话),随后是一个代码块,定义了对委托实例的操作。逆变性不适用于匿名方法,必须指定和委托类型完全匹配的参数类型(在本例中是两个Circle类型)。

    通过在匿名方法中加入return来获得返回值。.NET 2中很少有委托有返回值(因为多个委托形成委托链之后,前面的返回值会被后面的覆盖),但LINQ中大部分委托都有返回值(通过Func泛型委托)。

    使用匿名方法的主要好处是:不需要为一个函数命名,尤其是那种只用一次的函数,或者很短很简单的函数。当你了解了lambda表达式之后,就会发现在linq中,到处都是lambda表达式,而里面其实都是匿名函数(即委托)。如果我们在频繁使用linq的过程中,每次都要在外部建立一个函数,那代码的体积将会大大增加。

    另外匿名函数还有很重要的一点,就是自动形成闭包。匿名函数内定义的变量称为匿名函数的局部变量,和普通函数不同的是,匿名函数除了可以使用局部变量,传入的变量之外,还可以使用捕获变量。当外部的变量被匿名函数在函数方法中使用时,称为该变量被捕获(即它成为了一个捕获变量)。

    捕获的是变量的实例而不是值,也就是说,在匿名函数内的捕获变量和外部的变量是同一个。当变量被捕获时,值类型的变量自动“升级”,变成一个密封类。创建委托实例不会导致执行。

    捕获变量(Captured Variable)的作用

    捕获变量可以方便我们在创建匿名方法(或委托)时,获得所需要的变量。例如如果你有一个整型的列表,并希望写一个匿名方法筛选出小于某数limit的另一个列表,此时如果没有捕获变量,在匿名方法中我们就只能硬编码Limit的值,或者使用原始的委托,将变量传入委托的目标方法。

            static IEnumerable<int> Filter(List<int> aList, int limit)
            {
                //lambda表达式捕获外部变量Limit
                return aList.Where(a => a < limit);
            }

    捕获变量的生存期

    只要还有委托引用这个捕获变量,它就会一直存在。不管这个捕获变量是值类型还是引用类型,编译器会为其生成一个额外的类。

    public delegate void MethodInvoker();
            static void Main(string[] args)
            {
                MethodInvoker m = CreateDelegate();
                //由于有委托引用a,a将会一直存在
                //捕获变量a不再位于栈上,编译器将其视为一个额外的类
                //CreateDelegate方法拥有对这个额外的类的一个实例的引用
                //当委托被回收之前,不会回收这个额外的类
                m();
            }
    
            static MethodInvoker CreateDelegate()
            {
                int a = 1;
                MethodInvoker m = () =>
                {
                    Console.WriteLine(a);
                    a++;
                };
                m();
                return m;
            }

    打印出1和2。输出1是因为在调用CreateDelegate时,变量a是可用的。当CreateDelegate返回之后,调用m,a仍然是可用的,并没有随之消失。由于被捕获而形成闭包,a由一个栈上的值类型变成了引用类型。编译器生成了一个额外的密封类(名字是比较没有可读性的,例如c__DisplayClass1),它拥有一个成员a和一个方法,该方法内部的代码就是MethodInvoker中的代码。

    CreateDelegate持有一个类型c__DisplayClass1的引用,所以它一直都能使用c__DisplayClass1中的成员a。

        internal class Program
        {
            public delegate void MethodInvoker();
    
            [CompilerGenerated]
            private sealed class <>c__DisplayClass1
            {
                public int a;
    
                public void <CreateDelegate>b__0()
                {
                    Console.WriteLine(this.a);
                    this.a++;
                }
            }
    
            private static void Main(string[] args)
            {
                Program.MethodInvoker methodInvoker = Program.CreateDelegate();
                methodInvoker();
                Console.ReadKey();
            }
    
            private static Program.MethodInvoker CreateDelegate()
            {
                Program.<>c__DisplayClass1 <>c__DisplayClass = new Program.<>c__DisplayClass1();
                <>c__DisplayClass.a = 1;
                Program.MethodInvoker methodInvoker = new Program.MethodInvoker(<>c__DisplayClass.<CreateDelegate>b__0);
                methodInvoker();
                return methodInvoker;
            }
        }

    面试题:共享和非共享的捕获变量

    在闭包和for循环一起使用时,如果多个委托捕捉到了同一个变量,则会有两种情况:捕捉到了同一个变量仅有的一个实例,和捕捉到同一个变量,但每个委托拥有自己的一个实例。

            static void Main()
            {
                int copy;
                List<Action> actions = new List<Action>();
                for (int counter = 0; counter < 10; counter++)
                {
                    //只有一个变量copy,它在循环开始之前已经创建
                    //所有的委托共享这个变量
                    copy = counter;
                    //创建委托时不会执行
                    actions.Add(() => Console.WriteLine(copy));
                }
                foreach (Action action in actions)
                {
                    //执行委托时打印copy当前的值
                    //copy当前的值是9
                    action();
                }
                Console.ReadKey();
            }

    在这个例子中,捕获变量是copy,它只有一个实例(它的定义在外面,被捕获之后,自动升级为引用类型),所有委托共享这个实例。最后打印出10个9。

            static void Main()
            {
                int copy;
                List<Action> actions = new List<Action>();
                for (int counter = 0; counter < 10; counter++)
                {               
                    copy = counter;
                    //现在有十个内部变量,每个委托有一个实例,不同委托拥有的实例值是不同的
                    //从而委托可以输出0-9
                    int copy1 = copy;
                    //创建委托时不会执行
                    actions.Add(() => Console.WriteLine(copy1));
                }
                foreach (Action action in actions)
                {
                    //执行委托时打印copy1的值
                    action();
                }
                Console.ReadKey();
            }

    使用内部变量解决多个委托共享一个捕获变量实例的问题。下面的代码中,包含了上面所说的两种情况,可以思考下最终的打印结果:

            static void Main(string[] args)
            {
                var list = new List<MethodInvoker>();
                for (int index = 0; index < 5; index++)
                {
                    var counter = index*10;
    
                    list.Add(delegate
                    {
                        Console.WriteLine("{0}, {1}", counter, index);
                        counter++;    
                    });
                }
    
                list[0]();
                list[1]();
                list[2]();
                list[3]();
                list[4]();
    
                list[0]();
                list[0]();
                list[0]();
    
                Console.ReadKey();
            }

    其中循环内部建立了五个MethodInvoker。它们共享一个变量index的实例,但各自有自己的变量counter的实例。所以最终打印的结果中,index的值将总是5,而counter的值则每次都不同。

    最后额外执行了第一个委托三次,此时counter的值会使用第一次,第一个委托运行之后counter的值,故会打出1,之后打印2,3同理。如果你额外执行第二个委托一次,将会打出11。这充分说明了每个委托都持有一个counter的实例,且它们是相互独立的。而无论执行任意一个委托多少次,index的值都是5。

    foreach循环中捕获变量的变化

    在C# 5中,foreach循环的行为变了,不会再出现多个委托共享一个变量的行为。所以我们即使不声明内部变量,方法也会打印出令人容易理解的结果:

            static void Main()
            {
                List<string> values = new List<string> {"a", "b", "c"};
                var actions = new List<Action>();
                foreach (string s in values)
                {
                    //匿名方法捕获变量s
                    //类比for循环最后的10个9,s最后的值是c
                    //理论上会打印出三个c
                    //但在c# 5中,会打印出a,b,c
                    actions.Add(() => Console.WriteLine(s));
                }
                foreach (Action action in actions)
                {
                    action();
                }
                Console.ReadKey();
            }

    但对于for语句,行为和之前一样,仍然需要注意捕获变量被共享的问题。

  • 相关阅读:
    ActiveSync合作关系对话框的配置
    WINCE对象存储区(object store)
    Wince 隐藏TASKBAR的方法
    Wince输入法换肤换语言机制
    poj 3080 Blue Jeans 解题报告
    codeforces A. Vasily the Bear and Triangle 解题报告
    hdu 1050 Moving Tables 解题报告
    hdu 1113 Word Amalgamation 解题报告
    codeforces A. IQ Test 解题报告
    poj 1007 DNA Sorting 解题报告
  • 原文地址:https://www.cnblogs.com/haoyifei/p/5855602.html
Copyright © 2020-2023  润新知