推荐优先使用隐式类型的局部变量,即用var来声明,由于这能够使人把注意力放在最为重要的部分,也就是变量的语义上面,而不用分心去考虑其类型.数据库
用var来声明的变量不是动态变量,隐式类型的局部变量的类型推断也不等于动态类型检查。只是编译器会根据赋值符号右侧的表达式来推断变量的类型。var的意义在于不用专门指定变量的类型,而是交给编译器来判断,因此局部变量的类型推断机制并不影响C#的静态类型检查。
有时隐式类型会有比专门指定类型更好的表现,好比下面这段指定变量q为IEnumerable
public IEnumerable<string> FindCustomerStartWith(string start) { IEnumerable<string> q = from c in db.Customers select c.ContactName; var q2 = q.Select(a => a.StartsWith(start)); return q2; }
第一行查询语句会把每个人的姓名都从数据库里取出来,因为它要查询数据库,所以其返回值其实是IQueryable
而只须要改用var来声明变量,就能够避免这个问题:数组
public IEnumerable<string> FindCustomerStartWith(string start) { var q = from c in db.Customers select c.ContactName; var q2 = q.Select(a => a.StartsWith(start)); return q2; }
由于q变成了IQueryable
虽然推荐大多数时候使用var,但也不能盲目地使用var来声明一切局部变量。有时隐式类型可能带来一些隐秘的问题。由于若是用var来声明,则编译器会自行推断其类型,而其余开发者却看不到编译器所推断出的类型。所以,他们所认定的类型可能与编译器推断出的类型不符。这会令代码在维护过程当中遭到错误地修改,并产生一些原本能够避免的bug。
典型的如值类型,在计算过程当中可能会触发各类形式的转换。有些转换是宽化转换(widening conversion),这种转换确定是安全的,例如从float到double就是如此,但还有一些转换是窄化转换(narrowing conversion),这种转换会令精确度降低,例如从long到int的转换就会产生这个问题。若是明确地写出数值变量所应具有的类型,那么就能够更好地加以控制,并且编译器也会把有可能把因转换而丢失精度的地方给指出来。
好比下面这段代码:多线程
var f = GetMagicNumber(); var total = 100 * f / 6; Console.WriteLine($"Type: {total.GetType().Name}, Value: {total}");
下面这5种输出结果分别对应5个GetMagicNumber版本,每一个版本的返回值类型都不同:并发
Type: Double, Value: 1666.6666666666667 Type: Single, Value: 1666.6666 Type: Decimal, Value: 1666.6666666666666666666666667 Type: Int32, Value: 1666 Type: Int32, Value: 1666
total变量在这5种状况下会表现出5种不一样的类型,这是由于该变量的类型由变量f来肯定,而变量f的类型又是编译器根据GetMagicNumber()的返回值类型推断出来的。计算total值的时候,会用到一些常数,因为这些常数是以字面量的形式写出的,所以,编译器会将其转换成和f一致的类型,并按照那种类型的规则加以计算。因而,不一样的类型就会产生不一样的结果。异步
若是发现编译器自动选择的类型有可能使人误解代码的含义,令人没法马上看出这个局部变量的准确类型,那么就应该把类型明确指出来,而不要采用var来声明。反之,在其它的场景,都应该优先用var来声明局部变量。用隐式类型的局部变量来表示数值的时候要多加当心,由于可能会发生不少隐式转换,这不只容易令阅读代码的人产生误解,并且其中某些转换还会令精确度降低。ide
C#的常量有两种:函数
二者的区别主要有:
可见readonly比const更加灵活。此外,const在编译时解析值的特性还会对影响程序的维护工做。
好比在程序集A中有这样的代码:
public class ValueInfo{ public static readonly int Start = 5; public const int End = 10; }
而后程序集B引用了程序集A中的这两个常量:
for(var i = valueInfo.Start; i < valueInfo.End; i++) Console.Writeline(i);
则输出结果为:
5 6 7 8 9
随后修改了程序集A:
public class ValueInfo{ public static readonly int Start = 105; public const int End = 110; }
此后若是只发布程序集A,而不去构建程序集B,是不会下面这样获得指望的结果的:
105 106 ... 109
由于在程序集B中,valueInfo.End的值仍然是上一次编译是的10,要想让修改生效,须要从新编译程序集B。
推荐优先使用readonly,由于它比const更灵活,但const也不是一无可取,首先它的性能更好,此外有时使用const仅仅是为了消除魔数增长可读性,这种状况使用const也何尝不可,另外还有些确实须要在编译器把常量值固定下来的需求,那么也是必须使用const。
在C#中实现类型转换可使用as运算符,或者使用强制类型转换(cast)来绕过编译器的类型检查。
使用as运算符的写法:
private static void As() { // object a = null; object a = new TypeB(); var b = a as TypeA; if (b != null) { Console.WriteLine("convert succeed"); } else { Console.WriteLine("convert failed"); } }
使用cast的写法:
private static void Cast() { //object a = null; object a= new TypeB(); try { var b = (TypeA) a; if (b != null) { Console.WriteLine("convert succeed"); } else { Console.WriteLine("convert failed"); } } catch (InvalidCastException e) { Console.WriteLine("convert failed"); } }
TypeA与TypeB没有任何联系,所以两种写法的转换都会失败,但二者的区别在于:
object a = null
转换为TypeA时,二者的结果都是null因此a s写法在两种状况下的结果都是null,但cast写法须要判断null并catch InvalidCastException异常才能涵盖两种状况。可见as写法相比cast写法省了try/catch结构,程序的开销与代码量都比较低。除了判断转换结果是否为null,也能够先用Is来判断转换可否成功。
as与cast最大的区别在于它们如何对待由用户所定义的转换逻辑:
若是在TypeB类中定义以下运算符:
public class TypeB { private TypeA _typeA =new TypeA(); public static implicit operator TypeA(TypeB typeB) { return typeB._typeA; } }
那么前面的cast方式的代码应该就会把由用户所定义的转换逻辑也考虑进去,但运行后发现转换仍然失败,这是为何呢?
这是由于虽然cast方式会考虑自定义转换逻辑,但它针对的是源对象的编译期类型,而不是实际类型。具体到本例来讲,因为待转换的对象其编译期的类型是object,所以,编译器会把它当成object看待,而不考虑其在运行期的类型。
若是改为在cast前先转换为TypeB,则转换会成功:
... object a= new TypeB(); try { var a1 = a as TypeB; var b = (TypeA) a1; if (b != null) ...
但不推荐这种别扭的写法,应该优先考虑采用as运算符来实现类型转换,由于这样作要比盲目地进行类型转换更加安全,并且在运行的时候也更有效率。
相似下面这样的代码,将object转换为值类型,是没法经过语法检查的,由于值类型没法表示null:
object a = null; var b = a as int;
为此只需将转换目标修改成可空值类型就能够了:
object a = null; var b = a as int?;
使用面向对象语言来编程序的时候,应该尽可能避免类型转换操做,但总有一些场合是必须转换类型的。此时应该采用as及is运算符来更为清晰地表达代码的意图。
string.Format()能够用来设置字符串的格式,但C#6.0以后提供了内插字符串(Interpolated String)特性,更推荐使用后者。
$"the value of PI is {Math.PI}"
,会将double转换为string,因为double是值类型,必须先经过装箱操做转为object,若是这段代码频繁执行,就会严重影响性能。内插字符串其实是一种语法糖,生成的是FormattableString,将接收内插字符串的变量指定为FormattableString能够看到其Format属性的值,经过GetArguments能够看到对应的参数:
FormattableString a1 = $"the value of PI is {Math.PI}, E is {Math.E}"; Console.WriteLine("Format: " + a1.Format); Console.WriteLine("Arguments: "); foreach (var arg in a1.GetArguments()) { Console.WriteLine($"\t{arg}"); }
运行结果为:
Format: the value of PI is {0}, E is {1} Arguments: 3.141592653589793 2.718281828459045
只是在实际使用时系统会自动将其解读为string结果。
回调是一种由被调用端向调用端提供异步反馈的机制,它可能会涉及多线程(multithreading),也有可能只是给同步更新提供入口。
C#用委托来表示回调。经过委托,能够定义类型安全的回调。类型安全代码指访问被受权能够访问的内存位置,类型安全直观来讲意味着编译器将在编译时验证类型,若是尝试将错误的类型分配给变量,则抛出错误。
最经常使用到委托的地方是事件处理,此外,还可用于多种场合,好比想采用比接口更为松散的方式在类之间沟通时,就应该考虑委托。这种机制能够在运行的时候配置回调目标,而且可以通知给多个客户端。
委托是一种对象,其中含有指向方法的引用,这个方法既能够是静态方法,又能够是实例方法。
C#提供了一种简便的写法,能够直接用lambda表达式来表示委托。此外,还能够用Predicate
因为历史缘由,全部的委托都是多播委托(multicast delegate),也就是会把添加到委托中的全部目标函数(target function)都视为一个总体去执行。
这就须要注意下面两个问题:
程序在执行这些目标函数的过程当中可能发生异常;但多播委托在执行的时候,会依次调用这些目标函数,且不捕获异常。所以,只要其中一个目标抛出异常,调用链就会中断,从而致使其他的那些目标函数都得不到调用。
程序会把最后执行的那个目标函数所返回的结果当成整个委托的结果。
对于这两个问题,必要的时候能够经过委托的GetInvocationList
方法获取目标函数列表,而后手动遍从来处理异常和返回值。
关于事件处理程序,有不少陷阱要注意,好比,若是没有处理程序与这个事件相关联,那会出现什么状况?若是有多个线程都要检测并调用事件处理程序,而这些线程之间相互争夺,那又会出现什么状况?
触发事件的基本写法能够是这样:
public class EventSource { public event Action<int> Update; public void RaiseUpdate() { Update(2); } }
但若是没有为Update注册事件处理程序,这种写法就会报NullReferenceException,为此能够改进为触发前先检查事件处理程序是否存在:
public void RaiseUpdate() { if(Update!=null) Update(2); }
这种写法基本上能够应对各类情况,但仍是有个隐藏的bug。由于当程序中的线程执行完那行if语句并发现Updated不等于null以后,可能会有另外一个线程打断该线程,并将惟一的那个事件处理程序解除订阅,这样等早前的线程继续执行Updated(2)语句时,事件处理程序就变成了null,仍然会引起NullReferenceException。
为了预防这种状况出现,能够将代码继续改进为:
public void RaiseUpdate() { var handler = Update; if(handler!=null) handler(2); }
这种写法是线程安全的,由于将handler赋值为Update会执行浅拷贝,也就是建立新的引用,将handler指向原来Update的事件处理程序。这样即便另一个线程把Update事件清空,handler中仍是保存着事件处理程序的引用,并不会受到影响。
这种写法虽然没什么问题,但看起来冗长而费解。使用c#6.0引入的null条件运算符能够改用更为清晰的写法:
public void RaiseUpdate() { Update?.Invoke(2); }
这段代码采用null条件运算符(?.)首先判断其左侧的内容,若是不是null,那就执行右侧的内容,反之则跳过该语句。从语义上来看,这与前面的if结构相似,但区别在于条件运算符左侧的内容只会被计算一次。
值类型是盛放数据的容器,它们不该该设计成多态类型,但另外一方面,.NET又必须设计System.Object这样一种引用类型,并将其放在整个对象体系的根部,使得全部类型都成为由Object所派生出的多态类型。这两项目标是有所冲突的。
为了解决该冲突,.NET引入了装箱与拆箱的机制。装箱的过程是把值类型放在非类型化的引用对象中,使得那些须要使用引用类型的地方也可以使用值类型。拆箱则是把已经装箱的那个值拷贝一份出来。
若是要在只接受System.Object类型或接口类型的地方使用值类型,那就必然涉及装箱及取消装箱。
但这两项操做都很影响性能,有的时候还须要为对象建立临时的拷贝,并且容易给程序引入难于查找的bug。
所以,应该尽可能避免装箱与取消装箱这两种操做。
就连下面这条简单内插字符串写法都会用到装箱:
var firstNumber = 1; var a = $"the first number is: {firstNumber}";
由于系统在解读内插字符串时,须要建立由System.Object所构成的数组,以便将调用方所要输出的值放在这个数组里面,并交给由编译器所生成的方法去解读。但firstNumber变量倒是值类型,要想把它当成System.Object来用,就必须装箱。
此外,该方法的代码还须要调用ToString(),而这实际上至关于在箱子所封装的原值上面调用,也就是说,至关于生成了这样的代码:
var firstNumber = 1; object o = firstNumber; var str = firstNumber.ToString();
要避开这一点,须要提早把这些值手工地转换成string:
var a = $"the first number is: {firstNumber.ToString()}";
总之,要避免装箱与拆箱操做,就应注意那些会把值类型转换成System.Object类型的地方,例如把值类型的值放入集合、用值类型的值作参数来调用参数类型为System.Object的方法以及将这些值转为System.Object等。
new修饰符能够从新定义从基类继承下来的非虚成员,但要慎用这个特性,由于从新定义非虚方法可能会使程序表现出使人困惑的行为。
假设MyOtherClass继承自MyClass,那么初看起来下面这两种写法的效果应该是相同的:
object c = new MyOtherClass(); var c1 =c as MyClass; c1.MagicMethod(); var c2 =c as MyOtherClass; c2.MagicMethod();
但若是使用了new修饰符就不会相同了:
public class MyClass { public void MagicMethod() { Console.WriteLine("MyClass"); } } public class MyOtherClass : MyClass { public new void MagicMethod() { Console.WriteLine("MyOtherClass"); } }
c2.MagicMethod()
的结果是"MyOtherClass",
new修饰符并不会把原本是非虚的方法转变成虚方法,而是会在类的命名空间里面另外添加一个方法。非虚的方法是静态绑定的,因此凡是引用MyClass.MagicMethod()的地方到了运行的时候执行的都是MyClass类里面的那个MagicMethod,即使派生类里面还有其余版本的同名方法也不予考虑。
反之,虚方法则是动态绑定的,要到运行的时候才会根据对象的实际类型来决定应该调用哪一个版本。
不推荐new修饰符从新定义非虚的方法,但这并不是是在鼓励把基类的每一个方法都设置成虚方法。程序库的设计者若是把某个函数设置成虚函数,那至关于在制定契约,也就是要告诉使用者:该类的派生类可能会以其余的方式来实现这个虚函数。虚函数应该用来描述那些子类与基类可能有所区别的行为。若是直接把类中的全部函数全都设置成虚函数,那么就等于在说这个类的每一种行为都有可能为子类所修改。这表现出类的设计者根本就没有仔细去考虑其中到底有哪些行为才是真正可能会由子类来修改的。
本书的做者认为惟一一种可能使用new修饰符的状况是:新版的基类里面添加了一个方法,而那个方法与你的子类中已有的方法重名了。做者提到的缘由是:在这种状况下,你所写的代码里面可能已经有不少地方都用到了子类里面的这个方法,并且其余程序集或许也用到了这个方法,所以,想要给子类的方法更名可能比较麻烦。可是如今的IDE能够方便地重命名,并不会麻烦,因此new修饰符基本失去了使用场景,事实上,在平时也确实鲜有须要用到这个修饰符的状况。
《Effective C#:改善C#代码的50个有效方法(原书第3版)》 比尔·瓦格纳