基本语言要素
如何操作字符串? 如何进行转型? 什么是克隆?什么是相等型?为什么需要HashCode?
建议1 : 正确操作字符串
装箱所需要的步骤:
- 会为值类型在托管堆中分配内存. (还需要加上类型对象指针和同步块索引所占用的内存)
- 将值类型的值复制到新分配的堆内存中.
- 返回已经成为引用类型的对象的地址.
从以下两个方面来规避操作字符串额外的性能开销
- 确保尽量少的装箱
- 避免分配额外的内存空间
// -----------确保尽量少的装箱----------
// 会对Int32类型的9发生装箱
String str1 = "str1"+ 9;
// 调用的是整型的ToString方法
String str2 = "str2"+ 9.ToString();
//----------解析----------
// ToString方法原型为
return Number.FormatInt32(m_value, null, NumberFormatInfo.CurrentInfo);
// 此原型方法中不会发生装箱行为, 因为这是非托管方法,直接操作内存来完成int->String的转换
// 效率比装箱要高很多
[MethodImplAttribute(MethodImplOptions.InternalCall)]
public static extern String FormatInt32(int value, String format, NumberFormatInfo info);
//----------解析----------
因此在使用其他值引用类型
到字符串的转换并完成拼接时, 避免使用+操作符来完成, 而应该使用值引用类型
提供的ToString方法.
FCL方法内部也许会存在装箱的行为, 但是指导原则: 在自己编写的代码中, 应当尽可能避免编写不必要的装箱代码.
第二个方面: 避免分配额外的内存空间, 对CLR来说, String对象是个很特殊的对象, 一旦被赋值就不可改变. 在运行时调用String类中的任何方法或进行任何运算(=赋值,+拼接等), 都会在内存中创建一个新的字符串对象. 这也以为着要为该新对象分配新的内存空间.
//------------字符串操作---------------
private static void NewMethod1()
{
string s1 = "abc";
s1 = "123" + s1 + "456";
//以上两行代码创建了3个字符串对象,并执行了一次string.Contact方法
// IL_0001: ldstr "abc"
// IL_0006: stloc.0
// IL_0007: ldstr "123"
// IL_000c: ldloc.0
// IL_000d: ldstr "456"
// IL_0012: call string [System.Runtime]System.String::Concat(string, string,string,)
string s1 = "abc";
s1 = "123" + "456" + s1;
//以上两行代码创建了2个字符串对象,并执行了一次string.Contact方法
// IL_0000: nop
// IL_0001: ldstr "abc"
// IL_0006: stloc.0
// IL_0007: ldstr "123456"
// IL_000c: ldloc.0
// IL_000d: call string [System.Runtime]System.String::Concat(string,string)
}
private static void NewMethod6()
{
//该代码发生一次装箱,并调用一次string.Contact方法
string re6 = 9 + "456";
}
private static void NewMethod2()
{
//该代码等效于 string re2 = "123abc456";
// 不会在运行时拼接字符串,而是在编译时直接生产一个
string re2 = "123" + "abc" + "456";
}
private static void NewMethod9()
{
const string a = "t";
string re1 = "abc" + a;
//因为a是一个常量,因此
//该代码等效于 string re1 = "abc" + "t";
//最终等效于 string re1 = "abct";
}
StringBuilder
来弥补String
的不足. 它的效率源于预先以非托管的方式分配内存. 需要定义长度,如果没有分配,默认是16, 当字符串长度超过16时会重新分配内存使之成为16的倍数.
因此, 使用StringBuilder
指定的长度要合适, 太小了会频繁分配内存, 太大了浪费空间.
// String.Format方法内部使用了StringBuilder进行字符串的格式化
string a = "t";
string b = "e";
string c = "s";
string d = "t";
string.Format("{0}{1}{2}{3}", a, b, c, d);
建议2 : 使用默认的转型方法
如何正确地对类型实现转型?
在上一个建议中, int转型为string使用的int类型的ToString方法. 大部分情况下, 当需要对FCL提供的类型进行转型时,都应该使用FCL提供的转型方法.
- 使用类型的转换运算符
- 转换运算符分两类: 隐式转换和显示转换
- 也可以通过重载转换运算符的方式来提供这一类转换.
- 使用类型内置的Parse,TryParse或者ToString,ToDouble等方法.
- 使用帮助类提供的方法.
System.Convert
类支持任意自定义类型转换为任意基元类型,只要自定义类继承了IConvertible接口
就可以.
- 使用CLR支持的转型
- 上溯转型和下溯转型.(实际上就是基类和子类之间的相互转换)
// 1.重载转换运算符的方式来提供这一类转换.
class Ip
{
IPAddress value;
public Ip(string ip)
{
value = IPAddress.Parse(ip);
}
public static implicit operator Ip(string ip)
{
Ip iptemp = new Ip(ip);
return iptemp;
}
public override string ToString()
{
return value.ToString();
}
}
// 使用
Ip ip = "192.168.0.96";
Console.WriteLine(ip.ToString());
// 3. 使用帮助类提供的方法
// 可以使用System.Convert类,System.BitConverter类来进行类型转换.
// System.Convert类支持任意自定义类型转换为任意基元类型,只要自定义类继承了IConvertible接口就可以.
class Ip : IConvertible
{
// 省略
public bool ToBoolean(IFormatProvider provider)
{
throw new InvalidCastException("Ip-to-Boolean conversion is not supported.");
}
public string ToString(IFormatProvider provider)
{
return value.ToString();
}
// 省略
}
继承IConvertible接口必须同时实现其他转型方法, 如上文中的ToBoolean, 如果不支持此类型,则应该抛出一个InvalidCastException,而不是NotImplementedException.
// 4. 在进行子类向基类转型的时候可以隐式转换, dog显然就是一个Animal
// 当Animal转型为Dog时, 必须是显式转换, 因为还可能是一个Cat
Animal animal;
Dog dog = new Dog();
animal = dog; //隐式转化,因为Dog就是Animal。
//dog = animal; //编译不通过, animal可能会是一个cat
dog = (Dog)animal; //必须存在一个显式转换
建议3 : 区别对待强制转型与as与is
首先要明确什么是强制转型, 以及强制转型意味着什么.
secondType = (SecondType)firstType;
强制转型可能意味着两件不同的事情:
- FirstType和SecondType彼此依靠转换操作符来完成两个类型之间的转型
- FirstType是SecondType的基类.
它们之间的关系要么是第一种要么是第二种,不能同时既是继承又提供了转型符.
//------------------重载转换操作符----------------------
class FirstType
{
public string Name { get; set; }
}
class SecondType
{
public string Name { get; set; }
public static explicit operator SecondType(FirstType firstType)
{
SecondType secondType = new SecondType() { Name = "转型自:" + firstType.Name };
return secondType;
}
}
// 1.
// 想要转型成功则必须使用强制转型, 而不是使用as操作符.
FirstType firstType = new FirstType() { Name = "First Type" };
SecondType secondType = (SecondType)firstType; //转型成功
//secondType = firstType as SecondType; //编译期转型失败,编译不通过
// 2.
// 以下代码转型失败
// Object obj = firstType;
// SecondType secondType = (SecondType) obj;
// 3.
Object obj = firstType;
// 永远不会抛出异常
SecondType secondType = obj as SecondType;
// 如果类型不匹配, 既不是目标类型也不是其派生类型, 源对象或者为null
// 则转型之后的值为null
if (secondType != null) { ....; }
第二段代码与第一段代码比仅仅多了一层转型, 实际上obj还是firsrType,为什么转型就失败了呢? 因为编译器还不够聪明,或者说我们欺骗了编译器. 针对(SecondType) obj
, 编译器首先判断的是 SecondType
和object
之间是否有继承关系. C#中所有类型都继承自object的, 所以上述代码编译期没有问题, 但是编译器会自动生成代码来检查obj
在运行时
是不是SecondType
, 这样就绕过了转换操作符, 所以会转换失败.
用强制转型时, 在运行时调用重载的转换操作符方法.
如果多一层Object转型, 在编译期判断存在继承关系, 则会自动生成代码来判断是不是, 这样就不经过转换操作符方法了.
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
因此这里的建议是:
- 如果类型之间都上溯到某个共同的基类, 那么根据此基类进行的转换(即基类转型为子类本身)应该使用
as
- 子类与子类 之间的转型,则应该使用转换操作符,以便进行强制转型.
转型操作符实际上就是一个方法, 类型的转换需要手工写代码完成.
↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
第二种情况: FirstType是SecondType的基类: 既能使用强制转型, 也能使用as操作符. 从效率的角度来看,也建议大家使用as操作符.
以上就是强制转型和as的区别, 再看一下is操作符.
// 这个版本效率并不高, 进行了2次类型检测
static void DoWithSomeType(object obj)
{
if (obj is SecondType)
{
SecondType secondType = obj as SecondType;
//do something
}
}
as操作符存在一个问题, 即不能操作基元类型, 如果涉及基元类型的算法, 就需要通过is转型前的类型来进行判断,以免转型失败.
建议4 : TryParse比Parse好
除了String外的所有基元类型都有两个将字符串
转化为本身类型
的方法:
public static double Parse(string s);
public static bool TryParse(string s, out double result);
两者最大的区别是, 如果字符串不满足转换的要求, Parse方法将会引发一个异常; TryParse方法则不会引发异常, 会返回false,同时将result置为0.
早期没有TryParse方法, 出现异常时需要捕捉并设定成一个初始值. 引发异常这个过程会对性能造成损耗. 因此才提供了TryParse方法
static void Main(string[] args)
{
double re;
long ticks;
Stopwatch sw = Stopwatch.StartNew();
for (int i = 1; i < 1000; i++)
{
try
{
re = double.Parse("123");
}
catch
{
re = 0;
}
}
sw.Stop();
ticks = sw.ElapsedTicks;
Console.WriteLine("double.Parse() 成功,{0} ticks", ticks);
sw = Stopwatch.StartNew();
for (int i = 1; i < 1000; i++)
{
if (double.TryParse("123", out re) == false)
{
re = 0;
}
}
sw.Stop();
ticks = sw.ElapsedTicks;
Console.WriteLine("double.TryParse() 成功,{0} ticks", ticks);
sw = Stopwatch.StartNew();
for (int i = 1; i < 1000; i++)
{
try
{
re = double.Parse("aaa");
}
catch
{
re = 0;
}
}
sw.Stop();
ticks = sw.ElapsedTicks;
Console.WriteLine("double.Parse() 失败,{0} ticks", ticks);
sw = Stopwatch.StartNew();
for (int i = 1; i < 1000; i++)
{
if (double.TryParse("aaa", out re) == false)
{
re = 0;
}
}
sw.Stop();
ticks = sw.ElapsedTicks;
Console.WriteLine("double.TryParse() 失败,{0} ticks", ticks);
}
// double.Parse() 成功,1190 ticks
// double.TryParse() 成功,292 ticks
// double.Parse() 失败,101049 ticks
// double.TryParse() 失败,253 ticks
从上述结果可以看出, 效率TryParse()
最好. 并不建议所有的类型都提供TryParse模式, 只有在考虑到Do方法会带来明显的性能损耗时,才建议使用TryParse.
建议5 : 使用int?来确保值类型也可以为null
基元类型为什么需要为null? 考虑以下2个场景:
数据库中一个int字段可以被设置为null. 在C#中, 值从数据库被取出来后, 为了将它赋值给int类型, 不得不首先判断一下它是否为null, 如果直接赋值null给它会引发异常.
在一个分布式系统中, 服务器需要接受并解析来自客户端的数据. 一个Int型数据可能在传输过程中丢失或被篡改了, 转型失败后应该保存为null值, 而不是提供一个初始值.
所以从.NET 2.0开始,FCL中提供了一个额外的类型: 可空类型Nullable<T>
, 它是一个结构体.
// 两种写法, i的值的范围-2147483648~2147483647 再加上一个null值.
Nullable<Int32> i;
Int32? i;
// 可空类型与基元类型的互相转换. 基元类型提供了其对应的可空类型的隐式转换
int? i = null;
int j = 0;
i = j; // 默认提供了隐式转换
// 反过来, 需要以下的形式进行转换
if(i.HasValue)
j = i.Value;
elss
j = 0;
// 使用??运算符, ??运算符最大的用处就是将可空类型的值赋值给对应的基元类型进行简化.
j = i ?? 0; // 如果i.HasValue为true,则将i.Value赋值给j,否则就赋值为0.
??运算符
最大的用处就是将可空类型
的值赋值给对应的基元类型进行简化.
区别readonly和const的使用方法
在作者看来, 要使用const的理由只有一个, 那就是效率. 但是在大部分应用情况下, 效率并没有那么高的地位,所以我更愿意采用readonly, 因为readonly赋予代码更多的灵活性.
本质区别如下:
- const是一个编译期常量, readonly是一个运行时常量.
- const是编译期常量所以天然就是static的,不能再添加static修饰符.
- const只能修饰基元类型,枚举类型或字符串类型. readonly没有限制.
const之所以效率高是因为经过编译器编译之后,引用const变量的地方会用实际值来代替.
const int ConstValue = 100;
// 以下两行代码生成的IL代码是一致的
Console.WriteLine(ConstValue);
Console.WriteLine(100);
readonly变量是运行时变量, 其赋值行为发生在运行时. 它的全部意义在于, 运行时第一次被赋值后将不可以改变
. 此处有2个意思:
- 对于值类型变量, 值本身不可改变(readonly 只读)
- 对于引用类型变量, 引用本身(相当于指针)不可改变.
- 引用本身不可改变,引用所指的实例的值,却是可以改变的.
readonly所代表的运行时含义有一个重要的作用,就是可以为每个类的实例指定一个readonly变量. 以下面这个类为例, 可以在运行时生成多个实例, 而同时又可以为每个实例生成自己的readonly变量. 这就是readonly变量的灵活之处.
误区: readonly变量不能被重新赋值时不正确的. 下面的代码表明了它可以被改变.
// 在可以构造器方法内对readonly进行多次赋值.
// 实际上应该把初始化器也理解成构造方法的一部分, 它其实是一个语法糖.
class Sample
{
// 初始化器: 赋值为100
public readonly int ReadOnlyValue = 100;
// 构造方法中又可以对readonly多次赋值
public Sample(int value)
{
ReadOnlyValue = value;
}
}
建议7 : 将0值作为枚举的默认值
允许使用枚举的类型有byte,sbyte,ushort,int,uint,long,ulong. 应该始终将0值作为枚举类型的默认值.
例如,一个代表星期的枚举类Week,我们会想当然的认为它应该由7个元素.
enum Week
{
Monday = 1,
Tuesday = 2,
Wednesday = 3,
Thursday = 4,
Friday = 5,
Saturday = 6,
Sunday = 7
}
如果不小心编写了如下代码:
static Week week;
static void Main(string[] args)
{
// 输出0
Console.WriteLine(week);
// 如果枚举类型的元素类型为整型
// 这段代码并不会出错, 输出:9
week = (Week)9;
}
Week看上去多了第8个值, 同时很不幸,这段代码没有引发异常. 所有应该始终为枚举的0值指定默认值. 上述枚举的定义应该将显式为元素赋值去掉, 编译器会自动从0值开始计数, 然后逐个为元素的值+1.
建议8 : 避免给枚举类型的元素提供显式的值
一般情况下, 没有必要给枚举类型的元素提供显式的值, 创建枚举的理由之一,就是为了代替使用实际的数值.
不正确地为枚举类型的元素设定显式的值,会带来意想不到的错误.
enum Week
{
Monday = 1,
Tuesday = 2,
Temp,
Wednesday = 3,
Thursday = 4,
Friday = 5,
Saturday = 6,
Sunday = 7
}
// 为week赋值为Temp,可是得到的结果却是Wednesday
// 因为没有为元素显式赋值,编译器会逐个为元素的值+1
// 因此Temp和Wednesday的值都是3
Week week = Week.Temp;
Console.WriteLine(week);// Wednesday
Console.WriteLine(week == Week.Wednesday);//True
枚举元素允许设定重复的值.
enum Temp
{
Value1 = 1,
Value2 = 1
}
private static void NewMethod2()
{
Temp temp1 = Temp.Value1;
Temp temp2 = Temp.Value2;
Console.WriteLine(temp1 == temp2); // true
Console.WriteLine(temp1.Equals(temp2));// true
Console.WriteLine(temp1.CompareTo(temp2));// 0
Console.WriteLine(temp1 == Temp.Value1);// true
Console.WriteLine(temp1 == Temp.Value2);// true
}
本建议(避免给枚举类型的元素提供显式的值)也有例外, 就是指定[Flag]
特性的枚举类, 就意味着可以对这些值执行AND,OR,NOT和XOR按位运算, 这样一来, 就要求枚举的每个元素的值都是2的若干次幂,指数依次递增.
[Flags]
enum Week
{
None = 0x0,
Monday = 0x1,
Tuesday = 0x2,
Wednesday = 0x4,
Thursday = 0x8,
Friday = 0x10,
Saturday = 0x20,
Sunday = 0x40
}
Week week = Week.Thursday | Week.Sunday;
建议9 : 习惯重载运算符
重载运算符之后可以看起来一目了然,可以让开发人员像是用内置基元类型一样是用该类型.
通过operator关键字定义静态成员函数来重载运算符.
int x = 1;
int y = 2;
// 方式一, 都喜欢看到这样的语法特性
int total = x + y;
// 方式二
int total = int.Add(x,y);
Salary mikeIncome = new Salary() { RMB = 22 };
Salary roseIncome = new Salary() { RMB = 33 };
// 方式一
Salary familyIncome = Salary.Add(mikeIncome, roseIncome);
// 方式二: 重载运算符
Salary familyIncome = mikeIncome + roseIncome;
class Salary
{
public int RMB { get; set; }
public static Salary operator +(Salary s1, Salary s2)
{
s2.RMB += s1.RMB;
return s2;
}
}
建议10 : 创建对象时需要考虑是否实现比较器
有对象的地方就会存在比较, 比如, 有一个人的Salary列表, 根据排序需要, 列表要支持针对基本工资来罗列Salary, 这个时候接口IComparable就会起作用.
class Salary : IComparable
{
public string Name { get; set; }
// 基本工资
public int BaseSalary { get; set; }
// 奖金
public int Bonus { get; set; }
#region IComparable 成员
public int CompareTo(object obj)
{
Salary staff = obj as Salary;
if (BaseSalary > staff.BaseSalary)
{
return 1;
}
else if (BaseSalary == staff.BaseSalary)
{
return 0;
}
else
{
return -1;
}
// 以上代码可以用Int自带默认的比较方法来处理.
// return BaseSalary.CompareTo(staff.BaseSalary);
}
#endregion
}
// 如果不想以基本工资进行排序, 而是以奖金属性进行排序
// 这个时候, 接口IComparer的作用就体现出来了. 可以实现一个自定义的比较器
class BonusComparer : IComparer
{
#region IComparer 成员
public int Compare(object x, object y)
{
Salary s1 = x as Salary;
Salary s2 = y as Salary;
return s1.Bonus.CompareTo(s2.Bonus);
}
#endregion
}
// 排序使用
// 不建议使用非泛型集合类, 因为从上述代码发现, 会进行转型,影响性能,而泛型的出现可以避免运行时转型.
// ArrayList companySalary = new ArrayList();
// companySalary.Add(new Salary() { Name = "Mike", BaseSalary = 3000 });
// companySalary.Add(new Salary() { Name = "Rose", BaseSalary = 2000 });
// companySalary.Add(new Salary() { Name = "Jeffry", BaseSalary = 1000 });
// companySalary.Add(new Salary() { Name = "Steve", BaseSalary = 4000 });
// companySalary.Sort();
// companySalary.Sort(new BonusComparer()); //提供一个非默认的比较器
//foreach (Salary item in companySalary)
//{
// Console.WriteLine(string.Format("Name:{0} \tBaseSalary:{1} \tBonus:{2}", item.Name, item.BaseSalary, item.Bonus));
//}
// 因此上述代码应缓存List<T>,对应的,应该实现IComparable<T>和IComparer<T>接口
class Salary : IComparable<Salary>
{
public string Name { get; set; }
public int BaseSalary { get; set; }
public int Bonus { get; set; }
#region IComparable<Salary> 成员
public int CompareTo(Salary other)
{
return BaseSalary.CompareTo(other.BaseSalary);
}
#endregion
}
class BonusComparer : IComparer<Salary>
{
#region IComparer<Salary> 成员
public int Compare(Salary x, Salary y)
{
return x.Bonus.CompareTo(y.Bonus);
}
#endregion
}
建议11 : 区别对待 == 和Equals
在开始本建议之前,首先要明确概念”相等性”. CLR将相等性分为两类: 值相等性,引用相等性.
- 如果用来比较的两个变量所包含的数值相等, 那么将其定义为”值相等性”.
- 如果比较的两个变量引用的是内存中同一个对象, 那么将其定义为”引用相等性”.
无论”==”还是”Equals”都倾向于表达一个原则:
- 对于值类型, 如果类型的值相等,就应该返回True
- 对于引用类型,如果类型指向同一个对象, 则返回True
同时这个两个操作符都是可以被重载的. 比如string这样一个特殊的引用类型,微软觉得它的现实意义更接近于值类型,所在,在FCL中string的比较重载为针对类型的值
比较, 而不是针对引用本身
的比较.
从设计上来说, 很多自定义的类型(尤其是自定义的引用类型) 会存在和string类型比较接近的情况. 例如Person类, 如果两个Person对象的IDCode是相等的, 我们就认为两者是同一个人, 这时候就要重载Equals这个方法:
class Person
{
public string IDCode { get; private set; }
public Person(string idCode)
{
this.IDCode = idCode;
}
public override bool Equals(object obj)
{
return IDCode == (obj as Person).IDCode;
}
}
// 这里去比较2个person的对象
static void ReferenceTypeEquals()
{
object a = new Person("NB123");
object b = new Person("NB123");
//False
Console.WriteLine(a == b);
//True
Console.WriteLine(a.Equals(b));
}
一般来说, 要定义值相等性
应该仅仅去重载Equals方法, 同时让==
表示引用相等性
.
由于==
和Equals
都可以被重载表示值相等性
和引用相等性
, 所以,为了明确有一种方法肯定比较的是引用相等性
,FCL中提供了Object.ReferenceEquals
方法, 两个实例是否是同一个实例.
建议12 : 重写Equals时也要重写GetHashCode
除非考虑到自定义类型会被用作基于Hash的集合的键值, 否则不建议重写Equals方法, 因为这会带来一系列的问题.
如果重写Equals,没有重写GetHashCode, 在使用FCL中的Dictionary类时, Dictionary会根据Key值来查找Value值, CLR内部会优化这种查找, 实际上, 最终是根据Key值的HashCode来查找Value值.
代码运行时, CLR会调用Key对象的GetHashCode方法, 会根据类型计算出一个哈希值, 如果没有重写GetHashCode方法, 那么即使你重写Equals创建了2个你认为值相等性的对象, Dictionary则不会这么认为, 因此要重写Equals时也要重写GetHashCode.
同时,GetHashCode方法应该基于那些只读的属性或特性来生成HashCode, 如果改变Person的IDCode就相当于是另外一个人了.
GetHashCode方法还存在另外一个问题, 它永远只返回一个整数类型, 而整数类型的容量显然无法满足字符串的容量. 以下例子就能产生两个同一的HashCode:
string st1 = "NB0903100006";
string st2 = "NB0904140001";
Console.WriteLine(st1.GetHashCode()); // -1649519574
Console.WriteLine(st2.GetHashCode()); // -1649519574
为了减少两个不同类型之间根据字符串产生相同HashCode的几率, 要对GetHashCode方法进行改进
// 容易出相同HashCode的几率高
public override int GetHashCode()
{
return this.IDCode.GetHashCode();
}
// 改进后的方法
public override int GetHashCode()
{
return (System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName + "#" + this.IDCode).GetHashCode();
}
重写Equals方法时, 也应该实现一个类型安全的接口IEquatable
// 完整实现
class Person : IEquatable<Person>
{
public string IDCode { get; private set; }
public Person(string idCode)
{
this.IDCode = idCode;
}
public override bool Equals(object obj)
{
return IDCode == (obj as Person).IDCode;
}
public override int GetHashCode()
{
return (System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName + "#" + this.IDCode).GetHashCode();
}
public bool Equals(Person other)
{
return IDCode == other.IDCode;
}
}
建议13 : 为类型输出格式化字符串
两种方法可以为类型提供给格式化的字符串输出.
- 让类型继承接口IFormattable. 主动实现方式, 要求开发者可以预见类型在格式化方面的要求.
- 类型的使用者为类型自定义格式化器. 这是很多时候要用的方式,也是最灵活多变的方法.
最简单的字符串输出是为类型重写ToString方法, 如果没有为类型重写该方法, 默认会调用Object的ToString, 会返回当前类型的类型名称. 即使重写了ToString方法,字符串输出单一形式的字符串, 而通过实现IFormattable接口的ToString方法, 可以让类型根据用户的输入而格式化输出.
class Person : IFormattable
{
public string IDCode { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
//实现接口IFormattable的方法ToString
public string ToString(string format, IFormatProvider formatProvider)
{
switch (format)
{
case "Ch":
return this.ToString();
case "Eg":
return string.Format("{0} {1}", FirstName, LastName);
default:
return this.ToString();
}
}
//重写Object.ToString()
public override string ToString()
{
return string.Format("{0} {1}", LastName, FirstName);
}
}
Person person = new Person() { FirstName = "Jessica", LastName = "Hu", IDCode = "NB123" };
Console.WriteLine(person.ToString()); // Hu Jessica
PersonFomatter pFormatter = new PersonFomatter();
Console.WriteLine(pFormatter.Format("Ch", person, null));// Hu Jessica
Console.WriteLine(pFormatter.Format("Eg", person, null));// Jessica Hu
上面这种方法是在意识到类型会存在格式化字符串输出方面的需求时, 提前为类型继承了接口IFormattable.
第二种方式: 加入Person类是如下所示的实现:
class Person
{
public string IDCode { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
// 针对Person的格式化器的实现:
class PersonFomatter : IFormatProvider, ICustomFormatter
{
#region IFormatProvider 成员
public object GetFormat(Type formatType)
{
if (formatType == typeof(ICustomFormatter))
return this;
else
return null;
}
#endregion
#region ICustomFormatter 成员
public string Format(string format, object arg, IFormatProvider formatProvider)
{
Person person = arg as Person;
if (person == null)
{
return string.Empty;
}
switch (format)
{
case "Ch":
return string.Format("{0} {1}", person.LastName, person.FirstName);
case "Eg":
return string.Format("{0} {1}", person.FirstName, person.LastName);
case "ChM":
return string.Format("{0} {1} : {2}", person.LastName, person.FirstName, person.IDCode);
default:
return string.Format("{0} {1}", person.FirstName, person.LastName);
}
}
#endregion
}
//一个典型的格式化器应该继承接口IFormatProvider和ICustomFormatter
// 应该以如下方式调用
Person person = new Person() { FirstName = "Jessica", LastName = "Hu", IDCode = "NB123" };
Console.WriteLine(person.ToString()); // ConsoleApplication4.Person
PersonFomatter pFormatter = new PersonFomatter();
Console.WriteLine(pFormatter.Format("Ch", person, null));// Hu Jessica
Console.WriteLine(pFormatter.Format("Eg", person, null));//Jessica Hu
Console.WriteLine(pFormatter.Format("ChM", person, null));// Hu Jessica : NB 123
上述示例也演示了如果没有重写Object.ToString方法, 类型会输出类型名称.
在第一个版本Person类型中,对IFormattable的ToString方法稍作修改, 就能让格式化输出在语法上支持更多的调用方式. 以下是最终版本:
class Person : IFormattable
{
public string IDCode { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
//实现接口IFormattable的方法ToString
public string ToString(string format, IFormatProvider formatProvider)
{
switch (format)
{
case "Ch":
return this.ToString();
case "Eg":
return string.Format("{0} {1}", FirstName, LastName);
default:
//return this.ToString();
ICustomFormatter customFormatter = formatProvider as ICustomFormatter;
if (customFormatter == null)
{
return this.ToString();
}
return customFormatter.Format(format, this, null);
}
}
//重写Object.ToString()
public override string ToString()
{
return string.Format("{0} {1}", LastName, FirstName);
}
}
// 调用者的代码能够支持如下所示的语法
Person person = new Person() { FirstName = "Jessica", LastName = "Hu", IDCode = "NB123" };
Console.WriteLine(person.ToString());
PersonFomatter pFormatter = new PersonFomatter();
//第一类格式化输出语法
Console.WriteLine(pFormatter.Format("Ch", person, null));
Console.WriteLine(pFormatter.Format("Eg", person, null));
Console.WriteLine(pFormatter.Format("ChM", person, null));
//第二类格式化输出语法,也更简洁
Console.WriteLine(person.ToString("Ch", pFormatter));
Console.WriteLine(person.ToString("Eg", pFormatter));
Console.WriteLine(person.ToString("ChM", pFormatter));
建议14 : 正确实现浅拷贝和深拷贝
为对象创建副本的技术称为拷贝(也叫克隆). 拷贝分为浅拷贝和深拷贝.
- 浅拷贝: 值类型字段复制到副本后, 在副本修改不会影响到源对象对应的值, 但是引用类型字段被复制的是引用,而不是引用的对象, 在副本中对引用类型的字段做修改会影响到源对象本身.
- 深拷贝: 同样,将对象中的所有字段复制到新的对象中, 不过,无论是对象的值类型字段还是引用类型字段,都会被重新创建并赋值, 对副本的修改不会影响到源对象本身.
无论是浅拷贝还是深拷贝, 微软都建议用类型继承ICloneable
接口方式明确告诉调用者, 该类型可以被拷贝. 但是该接口只提供了一个Clone
方法, 无法表明是浅拷贝还是深拷贝.
class Employee : ICloneable
{
// 理论上string是引用类型, 但是由于该引用类型的特殊性(实现和语义上)
// MemberwiseClone仍旧为其创建了副本(而不是复制引用),
// 也就是说,在浅拷贝过程中, 应该将字符串看成值类型
public string IDCode { get; set; }
// 值类型
public int Age { get; set; }
// 引用类型, 浅拷贝只拷贝引用
public Department Department { get; set; }
#region ICloneable 成员
// 如果修改Department成员,会影响所有用此方法克隆出来的浅表副本
// 修改其余2个成员则不会.
public object Clone()
{
// 创建当前object的浅表副本
return this.MemberwiseClone();
}
#endregion
}
深拷贝有多种实现方法:
- 最简单的是手动对字段逐个进行赋值, 但是最容易出错, 如果类型的字段发生了变化或有增减, 那么该拷贝方法也需要发生相应的变化.
- 建议使用序列化形式来进行深拷贝
序列化方式如下:
class Employee : ICloneable
{
public string IDCode { get; set; }
public int Age { get; set; }
public Department Department { get; set; }
#region ICloneable 成员
public object Clone()
{
using (Stream objectStream = new MemoryStream())
{
IFormatter formatter = new BinaryFormatter();
formatter.Serialize(objectStream, this);
objectStream.Seek(0, SeekOrigin.Begin);
return formatter.Deserialize(objectStream) as Employee;
}
}
#endregion
}
该接口只提供了一个Clone
方法, 无法表明是浅拷贝还是深拷贝. 如果要在一个类中同时实现深拷贝和浅拷贝, 只能由我们自己实现两个额外的方法声明为DeepClone和Shallow, 因此, Employee的最终版本看起来应该像如下的形式:
[Serializable]
class Employee : ICloneable
{
public string IDCode { get; set; }
public int Age { get; set; }
public Department Department { get; set; }
#region ICloneable 成员
// 浅拷贝
public object Clone()
{
return this.MemberwiseClone();
}
#endregion
// 深拷贝
public Employee DeepClone()
{
using (Stream objectStream = new MemoryStream())
{
IFormatter formatter = new BinaryFormatter();
formatter.Serialize(objectStream, this);
objectStream.Seek(0, SeekOrigin.Begin);
return formatter.Deserialize(objectStream) as Employee;
}
}
// 浅拷贝
public Employee ShallowClone()
{
return Clone() as Employee;
}
}
建议15 : 使用dynamic来简化反射实现
dynamic的出现让C#具有了弱语言类型的特性. 编译器在编译的时候不再对类型进行检查, 编译器默认dynamic对象支持开发者想要的任何特性. 例如:
// 即使你对GetDynamicObject方法返回的对象一无所知, 也可以进行如下的调用, 编译器不会报错
dynamic dynamicObject = GetDynamicObject();
Console.WriteLine(dynamicObject.Name);
Console.WriteLine(dynamicObject.SamleMethod());
当然,如果在运行时dynamicObject不包含指定的这些方法属性, 运行时会抛出RuntimeBinderException
异常.
有人会将var关键字与dynamic进行比较, 实际上完全是两个概念,var是编译期抛给我们的语法糖,一旦被编译,编译期会自动匹配var变量的实际类型, 并用实际类型来替换该变量的声明, 这看上去就是在编码的时候是用实际类型进行声明的.
dynamic被编译后. 实际是一个object类型, 只不过编译期会对dynamic进行特殊的处理, 让它在编译期间不进行任何的类型检查, 而是将类型检查放到了运行期, 因此在vs中不支持智能感知, 而var是支持的
利用dynamic的这个特性, 可以简化C#中的反射语法. 在dynamic出现之前. 假设存在类,代码如下所示:
public class DynamicSample
{
public string Name { get; set; }
public int Add(int a, int b)
{
return a + b;
}
}
我们这样使用反射, 调用方代码如下:
static void Main(string[] args)
{
//-------------------普通反射用法-----------------------
int times = 1000000;
DynamicSample reflectSample = new DynamicSample();
var addMethod = typeof(DynamicSample).GetMethod("Add");
//--------------测试1000000次反射调用的耗时-----------------------
Stopwatch watch1 = Stopwatch.StartNew();
for (var i = 0; i < times; i++)
{
// object参数拆箱成int类型
addMethod.Invoke(reflectSample, new object[] { 1, 2 });
}
Console.WriteLine(string.Format("反射耗时:{0} 毫秒", watch1.ElapsedMilliseconds)); // 反射耗时:275 毫秒
//---------使用dynamic, 并且在可控的范围内减少一次拆箱的机会----
dynamic dynamicSample = new DynamicSample();
Stopwatch watch2 = Stopwatch.StartNew();
for (int i = 0; i < times; i++)
{
dynamicSample.Add(1, 2);
}
Console.WriteLine(string.Format("dynamic耗时:{0} 毫秒", watch2.ElapsedMilliseconds));// dynamic耗时:39 毫秒
//-------------------------------优化反射调用-------
// 利用委托来调用
DynamicSample reflectSampleBetter = new DynamicSample();
var addMethod2 = typeof(DynamicSample).GetMethod("Add");
// 创建一个返回int的委托func
var delg = (Func<DynamicSample, int, int, int>)Delegate.CreateDelegate(typeof(Func<DynamicSample, int, int, int>), addMethod2);
Stopwatch watch3 = Stopwatch.StartNew();
for (var i = 0; i < times; i++)
{
delg(reflectSampleBetter, 1, 2);
}
Console.WriteLine(string.Format("优化的反射耗时:{0} 毫秒", watch3.ElapsedMilliseconds));
}
// 使用dynamic效率高于没有优化的反射的实现效果
// 对反射进行优化, 效率略高于dynamic,但是牺牲了代码的整洁度
// 反射耗时:275 毫秒
// dynamic耗时:39 毫秒
// 优化的反射耗时:3 毫秒
从上述结果来看, 建议大家始终使用dynamic来简化反射的实现. 如果使用优化后的反射, 会牺牲代码的整洁度.