配色: 字号:
Chobits的文集
2016-09-09 | 阅:  转:  |  分享 
  
Chobits的文集

.NET面试题系列目录



隐式类型的局部变量



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



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



当变量的类型太长或者难以推测,但类型本身不重要时,比如你的LINQ语句中用了Groupby,那么一般来说基本很少人可以准确地推测出结果的类型吧。。。

当变量初始化时,此时可以根据new后面的类型得知变量类型,故不会对可读性造成影响

在Foreach循环中你迭代的对象,此时一般不需要显式指出类型

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



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



自动实现的属性



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





publicstructFoo

{

publicinta{get;privateset;}



Foo(intA):this()

{

a=A;

}

}



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







匿名类型(AnonymousType)



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



staticvoidMain(string[]args)

{

vartom=new{Name="Tom",Age=15};

Console.WriteLine("{0}:{1}",tom.Name,tom.Age);

}

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





staticvoidMain(string[]args)

{

varfamily=new[]

{

new{Name="Tom",Age=15},

new{Name="Jerry",Age=16}

};

varcat=new{Age=27,Name="Cat"};

vardog=new{Age=2222222222222222,Name="Dog"};



}



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



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



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



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



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



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



3//常数表达式

a//变量或参数表达式

!a//一元逻辑非表达式

a+b//二元加法表达式

Math.Sin(a)//方法调用(lambda)表达式

newStringBuilder()//new表达式

表达式的一个重要的特点是它可以无限组合,只要符合正确的类型和语义。表达式树则是将表达式转换为树形结构,其中每个节点都是表达式。表达式树通常被用于转换为其他形式的代码。例如LINQtoSQL将表达式树转译为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就表示各种二元运算符(例如加减乘除)的表达式。它需要两个运算数(注意运算数也是表达式):



publicstaticBinaryExpressionAdd(Expressionleft,Expressionright);

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



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



ConstantExpressionexp1=Expression.Constant(1);

ConstantExpressionexp2=Expression.Constant(2);

BinaryExpressionexp12=Expression.Add(exp1,exp2);

ConstantExpressionexp3=Expression.Constant(3);

BinaryExpressionexp123=Expression.Add(exp12,exp3);

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







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



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



代码如下:



ParameterExpressionexpA=Expression.Parameter(typeof(double),"a");//参数a

MethodCallExpressionexpCall=Expression.Call(typeof(Math).GetMethod("Sin",BindingFlags.Static|BindingFlags.Public),expA);//Math.Sin(a)

LambdaExpressionexp=Expression.Lambda(expCall,expA);//a=>Math.Sin(a)





使用Lambda表达式:通过Expression



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



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



Expression>exp2=d=>Math.Sin(d);

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



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



Expression>exp2=d=>Math.Sin(d);

Funcfunc=exp2.Compile();

Console.WriteLine(func(0.5));

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



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



代码法:





//(a,b,m,n)=>maa+nbb

ParameterExpressionexpA=Expression.Parameter(typeof(double),"a");//参数a

ParameterExpressionexpB=Expression.Parameter(typeof(double),"b");//参数b

ParameterExpressionexpM=Expression.Parameter(typeof(double),"m");//参数m

ParameterExpressionexpN=Expression.Parameter(typeof(double),"n");//参数n



BinaryExpressionmultiply1=Expression.Multiply(expM,expA);

BinaryExpressionmultiply2=Expression.Multiply(multiply1,expA);

BinaryExpressionmultiply3=Expression.Multiply(expN,expB);

BinaryExpressionmultiply4=Expression.Multiply(multiply3,expB);

BinaryExpressionadd=Expression.Add(multiply2,multiply4);



委托法:



Expression>exp4=(a,b,m,n)=>maa+nbb;



varret=exp4.Compile();

Console.WriteLine(ret.Invoke(1,2,3,4));//=311+422=3+16=19

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



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



参考资料:表达式树上手指南

扩展方法(ExtensionMethod)



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



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



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



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



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



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



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



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



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



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





staticvoidMain(string[]args)

{

//外部变量

vari=0;

//lambda表达式捕获外部变量

//在外部变量的作用域内声明了一个方法

MethodInvokerm=()=>

{

//使用外部变量

i=i+1;

};



m.Invoke();

//打印出1

Console.WriteLine(i);

}



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



匿名函数(AnonymousFunction)



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



例如我们可以这样写:



Compare(c1,c2,delegate(Circlea,Circleb)

{

if(a.Radius>b.Radius)return1;

if(a.Radius
return0;

});

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



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



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



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



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



捕获变量(CapturedVariable)的作用



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



staticIEnumerableFilter(ListaList,intlimit)

{

//lambda表达式捕获外部变量Limit

returnaList.Where(a=>a
}

捕获变量的生存期



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





publicdelegatevoidMethodInvoker();

staticvoidMain(string[]args)

{

MethodInvokerm=CreateDelegate();

//由于有委托引用a,a将会一直存在

//捕获变量a不再位于栈上,编译器将其视为一个额外的类

//CreateDelegate方法拥有对这个额外的类的一个实例的引用

//当委托被回收之前,不会回收这个额外的类

m();

}



staticMethodInvokerCreateDelegate()

{

inta=1;

MethodInvokerm=()=>

{

Console.WriteLine(a);

a++;

};

m();

returnm;

}



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



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





internalclassProgram

{

publicdelegatevoidMethodInvoker();



[CompilerGenerated]

privatesealedclass<>c_www.visa158.com_DisplayClass1

{

publicinta;



publicvoidb_www.hunanwang.net_0()

{

Console.WriteLine(this.a);

this.a++;

}

}



privatestaticvoidMain(string[]args)

{

Program.MethodInvokermethodInvoker=Program.CreateDelegate();

methodInvoker();

Console.ReadKey();

}



privatestaticProgram.MethodInvokerCreateDelegate()

{

Program.<>c__DisplayClass1<>c__DisplayClass=newProgram.<>c__DisplayClass1();

<>c__DisplayClass.a=1;

Program.MethodInvokermethodInvoker=newProgram.MethodInvoker(<>c__DisplayClass.b__0);

methodInvoker();

returnmethodInvoker;

}

}



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



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





staticvoidMain()

{

intcopy;

Listactions=newList();

for(intcounter=0;counter<10;counter++)

{

//只有一个变量copy,它在循环开始之前已经创建

//所有的委托共享这个变量

copy=counter;

//创建委托时不会执行

actions.Add(()=>Console.WriteLine(copy));

}

foreach(Actionactioninactions)

{

//执行委托时打印copy当前的值

//copy当前的值是9

action();

}

Console.ReadKey();

}



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





staticvoidMain()

{

intcopy;

Listactions=newList();

for(intcounter=0;counter<10;counter++)

{

copy=counter;

//现在有十个内部变量,每个委托有一个实例,不同委托拥有的实例值是不同的

//从而委托可以输出0-9

intcopy1=copy;

//创建委托时不会执行

actions.Add(()=>Console.WriteLine(copy1));

}

foreach(Actionactioninactions)

{

//执行委托时打印copy1的值

action();

}

Console.ReadKey();

}



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





staticvoidMain(string[]args)

{

varlist=newList();

for(intindex=0;index<5;index++)

{

varcounter=index10;



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循环的行为变了,不会再出现多个委托共享一个变量的行为。所以我们即使不声明内部变量,方法也会打印出令人容易理解的结果:









staticvoidMain()

{

Listvalues=newList{"a","b","c"};

varactions=newList();

foreach(stringsinvalues)

{

//匿名方法捕获变量s

//类比for循环最后的10个9,s最后的值是c

//理论上会打印出三个c

//但在c#5中,会打印出a,b,c

actions.Add(()=>Console.WriteLine(s));

}

foreach(Actionactioninactions)

{

action();

}

Console.ReadKey();

}



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

献花(0)
+1
(本文系白狐一梦首藏)