C#高级编程第11版 - 第六章 索引
阅读原文时间:2022年04月21日阅读:2

【1】6.2 运算符

1.&符在C#里是逻辑与运算。管道符号|在C#里则是逻辑或运算。%运算符用来返回除法运算的余数,因此当x=7时,x%5的值将是2。

【2】6.2.1 运算符的简写

1.下面的例子++运算符来演示前缀式和后缀式之间的不同表现:

int x = 5;
if (++x == 6) // true – x先自加,再进行判断,此时x为6,因此为true。
{
Console.WriteLine("This will execute");
}
if (x++ == 7) // false – x先判断是否等于7,此时x为6,不等于7,所以为false,最后x自加,变成7
{
Console.WriteLine("This won't");
}

【3】6.2.2 条件表达式运算符 ( ? : )

条件表达式运算符 ( ? : ),也被成为三元运算符,是if…else代码段的速写方式。

语法如下所示:

condition ? true_value : false_value

【4】6.2.3 checked和unchecked

1.checked。CLR将会强制进行数据溢出检查,一旦发生溢出,就会直接抛出一个OverflowException。正如下面的例子所示:

byte b = 255;
checked
{
b++; // System.OverflowException: Arithmetic operation resulted in an overflow.
}
Console.WriteLine(b);

也可以在VS项目属性设置是否要进行算数运算的溢出检查。

2.如果你想忽略溢出检查,你可以将指定代码段标记为unchecked:

byte b = 255;
unchecked
{
b++;
}
Console.WriteLine(b);

unchecked是默认操作。只有当你在一个显式标记为checked的大代码段内需要忽略某些数据溢出的时候,你才需要显式指定unchecked。

3.默认情况下,上下限溢出并不会被检查因为它对性能有影响。当你为你的项目使用默认的check操作时,每个算数运算的结果都会被确认,无论它是否会有溢出。即使是for语句里常见的i++语句也不例外。为了不过度的影响程序性能,将溢出检查设置为默认不检查是更好的方案,你只需要在需要的地方使用checked运算符即可。

【5】6.2.4 is运算符

1.is运算符允许你检查某个实例是否兼容(compatible)某个指定类型。可以用is来检查常量,类型和变量。

2.通过使用is运算符,判断类型是否匹配的时候,可以在类型的右侧声明一个变量。假如is运算符的表达式计算结果为true,变量将会指向类型的实例。

public static void AMethodUsingPatternMatching(object o)
{
if (o is Person p)
{
Console.WriteLine($"o is a Person with firstname {p.FirstName}");
}
}
//…
AMethodUsingPatternMatching (new Person("Katharina", "Nagel"));

【6】6.2.5 as运算符

1.as运算符用在引用类型上,用来显示地进行类型转换。如果某个对象可以转换成指定类型,as就会成功执行。假如类型不匹配,as运算符将会返回一个null值。

2.as操作符允许你在一步内执行安全的类型转换,而不用事先通过is运算符进行判断再进行类型转换。

【7】6.2.6 sizeof运算符

1.通过sizeof运算符来决定一个值类型在栈里存储的内存大小(以byte为单位):

Console.WriteLine(sizeof(int));

上面这句代码将会输出4,因为int是4字节长度。

2.假如struct值拥有值类型变量的时候,你也可以对struct使用sizeof运算符。

3.不能将sizeof运算符用在class上。

4.默认情况下是不允许书写不安全的代码的,你需要在"项目->生成"中勾选上"允许不安全代码",或者在csproj项目文件中添加上标签,设置为true。

5.为了在代码中使用sizeof,你需要显式声明一个unsafe代码块:

unsafe
{
Console.WriteLine(sizeof(Point));
}

【8】6.2.7 typeof运算符

typeof运算符将会返回一个System.Type类型的实例用来代表指定类型。例如,typeof(string)返回的是一个Type对象,用来代表System.String类型。这点在你通过反射技术从一个对象中动态查找指定信息时非常有用。

【9】6.2.8 nameof运算符

1.这个运算符接收一个标识符,属性或者方法,返回相应的名称。当你需要用到一个变量名称的时候:

public void Method(object o)
{
if (o == null) throw new ArgumentNullException(nameof(o));
}

2.简单地使用字符串名称的时候,假如你拼错了某个单词,是不会引起任何编译错误的。这个nameof运算符则可以解决这个问题。

【10】6.2.9 index运算符

用到索引运算符(中括号)来访问数组:

int[] arr1 = {1, 2, 3, 4};
int x = arr1[2]; // x == 3

【11】6.2.10 可空类型与运算符

1.值类型和引用类型中一个很重要的区别就是引用类型可以为null值。

2.每个结构体都可以被定义为可空类型,就像long?DateTime?

long? l1 = null;
DateTime? d1 = null;

3.假如你在代码中使用了可空类型,你必须时刻考虑可空类型面对不同运算符时null值的影响。通常,当使用一元(unary)或者二进制运算符配合可空类型进行运算时,只要有一个操作数为null的话,结果往往也为null:

int? a = null;
int? b = a + 4; // b = null
int? c = a * 5; // c = null

4.当对可空类型进行比较时,假如只有一个操作数为null,则比较结果为false。这意味着某些时候你不需要考虑结果为true的可能,只需要考虑它的反面肯定是false的情况即可。这种情况经常发生在和正常类型进行比较时。举个例子,在下面的例子里,只要a为null值,则else语句总是为true的不管b就是是+5还是-5。

int? a = null;
int? b = -5;
if (a >= b) // if a or b is null, this condition is false
{
Console.WriteLine("a >= b");
}
else
{
Console.WriteLine("a < b");
}

5.可能为null值意味着你不能简单地在一个表达式里同时使用可空类型和非可空类型。

6.当然你使用在C#的类型定义后面使用关键字?,如int?,编译器将这个类型处理成泛型类型Nullable。编译器将简写的方式替换成泛型类型减少类型转换的开销。

【12】6.2.11 空值合并运算符 ( ?? )

1.空值合并(coalescing)运算符 ( ?? )提供了一种简写的方式,来满足处理可空类型或者引用类型为null值时的情况。这个运算符需要两个操作数,第一个操作数必须是可空类型或者引用类型,第二个操作数必须是第一个操作数同样的类型或者可以隐式转换成第一个操作数的类型。

2.合并运算符将按照以下规则进行求值:

  • 假如第一个操作数不为null,那么整个表达式的值就是第一个操作数的值。
  • 假如第一个操作数是null,则整个表达式将会以第二个操作数的值为准。

【13】6.2.12 空值条件运算符 ( ?. 和?[] )

1.使用空值条件运算符来访问FirstName属性的时候(p?.FirstName),当p为null时,只有null值会返回而不会继续执行右侧的表达式:

public void ShowPerson(Person p)
{
string firstName = p?.FirstName;
//…
}

当一个int类型的属性要使用空值条件运算符的时候,它的结果不能直接赋值给一个普通的int类型因为结果可能会为空,一个解决方案是赋值给可空int类型int?

int? age = p?.Age;

当然你也可以使用空值合并运算符来解决这个问题,通过定义一个缺省值(例如0)来应付万一左侧的表达式为null的情况:

int age1 = p?.Age ?? 0;

2.多个空值条件运算符也可以被合并。这里有一个Person对象,我们要访问它的Address属性,而这个属性又包含一个City属性,我们可能会先检查Person对象是否为空,然后再检查Address属性是否为空,都不为空的情况下我们才能访问到City属性:

Person p = GetPerson();
string city = null;
if (p != null && p.HomeAddress != null)
{
city = p.HomeAddress.City;
}

而当你使用空值条件运算符的时候,你就可以简单地这么写:

string city = p?.HomeAddress?.City;

3.通过使用?[]来访问数组。如下所示,假如arr数组为null,则不会接着执行[0]访问arr的第一个数组元素,直接就返回一个null值,并且这里使用了空值合并运算符??,当左侧的表达式返回为null时,我们将右侧的0赋值给x1:

int x1 = arr?[0] ?? 0;

【14】6.2.13 运算符的优先级和关联性

1.最典型的从右向左运算的例子就是赋值运算符。例如下面的代码,我们首先是将z的值赋值给y,然后将y的值再赋给x:

x = y = z

2.另外一个容易被误导的右联运算符是条件表达式运算符( ? : ),下面两个表达式是等价的:

a ? b : c ? d: e
a ? b : (c ? d: e)

【15】6.3 使用二进制运算符

一个按位与,按位或,按位异或,取反的二进制示例。

【16】6.3.1 移位

二进制每往左移动一位,相当于值乘以2。

【17】6.3.2 有符号数和无符号数

当你使用有符号类型,如int,long,short来存储二进制数时,它的左边第一位是用来代表符号的。当你使用int时,它所能代表的最大值为2147483647——也就是31位的1(二进制)或者0x7FFF FFFF。而使用uint的话,则最大值可以是4294967295——32位的1(二进制)或者0xFFFF FFFF。

【18】6.4.1 类型转换

两个转换机制——隐式(implicit)和显式(explicit)。

【19】6.4.1.1 隐式转换

1.可以隐式地将较小的数据类型转换成较大的数据类型,反过来则不行。

2.

可空数据类型在隐式转换为值类型的时候需要考虑更多一些:

  • 可空数据类型转换成其他可空类型就跟上面表格中的普通类型一样,譬如int?可以隐式地转换成long?float?double?decimal?
  • 普通类型隐式地转换成可空类型也与上面表格中定义的规则比较类似,譬如int类型可以隐式地转换成long?float?double?decimal?
  • 可空类型不能隐式地转换成普通类型。你必须使用显式转换。这是因为可空类型有可能代表null值,而普通类型无法存储null值。

【20】6.4.1.2 显式转换

1.你可以使用强制类型运算符来显式地处理这些转换。当你使用强制转换运算符时,你故意(deliberately)强制编译器进行此种转换,就像下面这样:

long val = 30000;
int i = (int)val; // 有效的强制转换,因为int的最大值为2147483647

2.可以使用checked运算符来保证强制转换的安全并且强制使运行时在溢出时抛出一个OverflowException异常:

long val = 3000000000;
int i = checked((int)val); // System.OverflowException: Arithmetic operation resulted in an overflow

3.在下面这段代码中,price的值增加了0.5,并且最后结果被强制转换为int类型:

double price = 25.30;
int approximatePrice = (int)(price + 0.5);

在这种转换中,小数部分的数据丢失了——换句话说,就是小数点之后的内容都丢失了。

4.试图将一个无符号整数转换成char类型时的情况:

ushort c = 43;
char symbol = (char)c;
Console.WriteLine(symbol); // +

5.转换数组元素,将它转换成某个结构体的成员变量:

struct ItemDetails
{
public string Description;
public int ApproxPrice;
}
//…
double[] Prices = { 25.30, 26.20, 27.40, 30.00 };
ItemDetails id;
id.Description = "Hello there.";
id.ApproxPrice = (int)(Prices[0] + 0.5);

6.将一个可空类型转换成普通类型或者另外一个可空类型可能会引起数据丢失,因此你需要使用显式强制转换。即使将可空类型强制转换成它的内置类型时也是这样的——例如int?转int或者float?转float的时候。这是因为可空类型可能存在null值,它不可能用普通类型来表示。只要在两个不相同的普通类型之间可以进行强制转换,那么相应的可空类型之间也可以进行强制转换。此外,当试图将一个值为null的可空类型强制转换为普通类型时,将会抛出一个InvalidOperationException,就像下面这样:

int? a = null;
int b = (int)a; // Nullable object must have a value.

值不为null或许就不会有InvalidOperationException?

7.就值类型来说,你只能在数值类型之间互相转换,或者与char和enum之间转换。你无法将Boolean直接转换成任何类型,反之亦然。

8.假如你需要将一个字符串转换成一个数值型或者布尔值,你可以使用预定义类型内置的Parse方法:

string s = "100";
int i = int.Parse(s);
Console.WriteLine(i + 50); // Add 50 to prove it is really an int

注意当Parse方法无法转换一个字符串时,它会抛出一个异常(例如你想将"Hello"转换成一个整数的时候)。

【21】6.4.2 装箱和拆箱

1.这个从值类型转换成引用类型的操作术语,就叫做装箱。基本来讲,运行时为所需的object对象创建了一个临时的引用类型盒子,并存储在托管堆上。这个转换是隐式的。

2.拆箱则是用来描述引用类型转值类型操作。这个操作必须是显式地。和我们曾经介绍过的强制转换很类似。

3.当拆箱的时候,你要小心拆箱后的类型是否容得下被装箱的原始类型。举个例子:C#的int类型,只能存储4字节的数据,假如你拆箱一个long值(8字节),赋值给一个int,像下面这样,同样会提示InvalidCastException,这点与long变量的实际值是否在int取值范围内无关:

long myLongNumber = 1;
object myObject = (object)myLongNumber;
int myIntNumber = (int)myObject; // Unable to cast object of type 'System.Int64' to type 'System.Int32'

myLongNumber

【22】6.5 比较对象是否相等

对象之间是否相等完全取决于你是比较两个引用类型(类的实例之间)或者是值类型(如基础数据类型,struct实例,或者枚举类型)。

【23】6.5.1 比较引用类型是否相等

1.System.Object定义了3个不同的方法用来比较对象是否相等:一个ReferenceEquals方法,一个静态Equals方法以及一个实例Equals虚方法。你也可以实现接口IEquality,它定义了一个Equals方法,带有一个代替object类型的泛型类型参数。除了这些之外,你还可以使用比较运算符==

2.ReferenceEquals是一个静态方法,用来测试是否两个引用都指向同一个类的实例,具体来说就是两个引用是否具有同一个内存地址。

3.作为一个静态方法,它无法被重写,所以System.Object类里的这个方法就是唯一的ReferenceEquals版本。当两个引用指向的是同一个对象实例时,ReferenceEquals会返回true,否则返回false。

4.null和null值也为true。

5.System.Object里实现了一个的Equals虚方法,也可以用来比较引用。

6.因为这个方法声明成了virtual,因此你也可以在你自己的类里面重写它,用来按照你自己的需要进行比较。尤其是,当你将你自己的类作为字典的键值的时候,你必须重写这个方法,以便能进行值的比较。另外,取决于你如何重写Object.GetHashCode,包含你自己类对象的字典可能会完全没法用或者非常低效。

7.注意当你重写Equals方法的时候,你的实现必须不会出现任何异常。进一步讲,如果你重写的Equals出异常将会引起一些其他的问题,因为不单单只是将你的类应用在字典中时会出问题,一些其他的.NET基础类也会内部调用到这个方法。

8.静态Equals方法和实例版本的Equals方法实际上干的事都一样。唯一的区别就是静态版本的Equals方法带有两个参数并且比较这两个参数是否相当,实例版本就一个参数而已。这个方法也可以用来处理两个参数都是null的清况。因此,它提供了额外的安全机制来确保当某个参数可能为null的时候不抛出异常。

9.静态Equals方法首先判断传递过来的是不是null,假如两个参数都是null,返回true(因为null被认为是与null相等的)。假如只有其中一个是null,则返回false。假如两个引用都有值,则调用实例版本的Equals方法。这意味着当你在自己的类中重写了Equals方法的话,效果跟你重写了静态版本一样。

10.比较运算符( == )。最好思考一下比较运算符作为严格值比较和严格对象比较之间的中间选项。

【24】6.5.2 比较值类型是否相等

1.ReferenceEquals用来比较引用,Equals用来比较值,而==运算符则是折衷方案(intermediate case)。

2.你自己创建的结构体,默认不支持==运算符重载。直接写sA == sB会导致一个编译错误,除非你在自己的代码里提供了==的重载。列sA和sB都是结构体的实例。

3.Microsoft已经在System.ValueType类重载了实例Equals方法,来测试值类型的相等性。

4.对值类型调用ReferenceEquals经常会返回false,这是因为调用这个方法的时候,值类型会被装箱成object类型。原因是这里分别进行了两次装箱,这意味着你获得的是两个不同的引用。因此,对于值类型来说没有必要调用ReferenceEquals进行比较因为它没有任何意义。

5.假如一个值类型包含引用类型作为字段,你可能想通过重写Equals方法为这些引用类型的字段提供更合适的应用场景,因为默认的Equals方法仅仅会简单地比较它们的内存地址。

【25】6.6 运算符重载

1.你不想经常调用某个类的属性或方法时,使用运算符重载会很有用。

2.重载不单单仅限于算数运算符。你同样需要考虑比较运算符,如==<!等等。

3.对于结构体struct来说,==运算符默认是无效的。尝试比较两个结构体是否相等只会产生一个编译错误,除非你显式地重载了==来告诉编译器如何实现这个比较。

4.合理使用运算符重载使你能够写出更具可读性并且更直观的代码。

【26】6.6.1 运算符的工作方式

1.int+uint=long+long

2.浮点数是通过尾数(mantissa)和指数(exponent)进行存储的。对它们进行相加将会包含位移动(bit-shifting)操作,以便两数拥有相同的指数,然后再将尾数进行相加,对结果的尾数进行转换,使得结果能包含最高的精度。

3.double+int=double+double

4.假如编译器能找到合适的重载,它将会调用运算符的实现。假如它找不到,它会检查是否有其他的+重载适合处理这种情况——可能有某些其他的类型实现了+重载,参数虽然不是Vector,但可以隐式地转换成Vector类型。假如编译器还是不能找到合适的重载方法,它将会抛出一个编译错误,因为它找不到任何重载方法可以处理这个操作。

【27】6.6.2 运算符重载的示例:Vector 结构

1.标量(scalar)。

2.在struct或者class里重载运算符并没有什么两样。

3.运算符重载,为Vector类提供了+运算:

public static Vector operator +(Vector left, Vector right) => new Vector(left.X + right.X, left.Y + right.Y, left.Z + right.Z);

运算符的重载和静态方法很像,除了多了一个operator关键字以外。这个关键字告诉编译器你定义了一个运算符重载。operator关键字后紧跟相关运算符的符号,这里我们用的是加法符号+。返回类型为你使用这个运算符后最终得到的类型,因为我们知道两个向量相加还是向量,因此返回类型为Vector。在这个特殊的加法重载例子中,返回类型与它的容器类型一致,但这并不是必然的,后续你可以看到返回值为其他类型的例子。两个参数left和right是你要用来做加法运算的操作数。对于二元运算符来说,如加法和减法,第一个参数是在运算符左边的,第二个参数则是在运算符右边。

4.向量与标量之间的乘法运算非常简单,就是向量的每个部分都单独地与标量进行乘法运算,如下所示:

public static Vector operator *(double left, Vector right) => new Vector(left * right.X, left * right.Y, left * right.Z);

5.注意运算符重载方法参数的位置,要相对应才不会出错。

6.数学运算符的重载并非只能返回所在类的类型。

7.C#要求所有的运算符重载必须声明成public和static,这意味着它们跟类或者结构体相关,而非具体的实例。因此,运算符重载的方法体无法直接访问非静态的内部成员或者使用this关键字。

8.+=虽然看上去是一个运算符,但其实它的计算分两步走:先进行加法运算,再进行赋值。跟C++不同的是,C#不允许你重载=运算符,但如果你重载了+运算符,编译器会自动应用你的+重载来实现+=操作。同样的原理也适用于其它的赋值运算符,譬如-=*=等等。

【28】6.6.3 比较运算符的重载

1.C#要求你成对地实现比较运算符,换句话说,假如你实现了==的重载,你就必须也实现!=,否则你将会得到一个编译错误。if(){}else{}

2.比较运算符的返回类型必须是bool类型。

3.请不要尝试在你重载的==中只简单地调用System.Object中实例版本的Equals方法来返回true或者false。假如你这么做了,当代码中试图进行(objA == objB)的比较时,有可能会报错。因为objA可能是null值,编译器实际上是试图执行null.Equals(objB)方法。你可以通过override实例版本的Equals方法来实现相等性的比较,这更安全一些。

4.对于值类型来说,你还必须实现IEquatable接口,比起基类object定义的Equals虚方法来说,这个接口定义的是强类型的版本,基于上面我们已经实现过的代码,你可以很简单地实现它:

private readonly struct Vector:IEquatable
{
//…
public bool Equals([AllowNull] Vector other) => this == other;
}

【29】6.6.4 可以重载的运算符

1.算术二元运算符,算术一元运算符,位二元运算符,位一元运算符,比较运算符,赋值运算符,索引器,类型强制转换。

2.你可能会对重载true和false运算符可以重载感到疑惑。事实上,一个整数值能否代表true或者false完全取决于你所使用的技术或者框架。在很多技术中,0代表着false而1代表着true;而有些技术则定义0为false,非0则为true。当你为某个类型实现了true或者false重载,那么该类型的实例直接就可以作为条件语句的判断条件。

【30】6.7 实现自定义的索引运算符

1.索引器看起来跟属性非常的相似,因为它也包含了get和set访问器。唯一的区别就是名称。定义一个索引器会用到this关键字。紧随其后的中括号则指定了索引的数据类型:

public Person this[int index]
{
get => _people[index];
set => _people[index] = value;
}

2.在索引器中,你并非只能定义int类型的索引参数。任何类型都可以,譬如下面这个例子:

public IEnumerable this[DateTime birthDay]
{
get => _people.Where(p => p.Birthday == birthDay);
}

里我们用DateTime类型作为索引参数,并且我们返回的并非单一的Person,而是一个可枚举的Person集合。

索引器使用DateTime类型提供了Person实例的取值,但你无法对其进行赋值,因为它并没有提供set访问器。上面的代码也可以用表达式的方式简写成:

public IEnumerable this[DateTime birthDay] => _people.Where(p => p.Birthday == birthDay);

【31】6.8 用户定义的类型强制转换

1.对于预定义数据类型来说,最好的方式是使用显式转换,因为有时候转换会失败,又或者丢失某些数据。

2.定义强制转换的语法和重载运算符的语法很像。这并非巧合——强制转换也被当成一种运算符,从一种类型转换成另外一种类型。为了演示语法,接下来的例子我们将用到一个叫Currency的结构体:

public static implicit operator float (Currency value)
{
// processing
}

这里定义的强制转换允许你隐式地将一个Currency值转换成一个float。注意当一个转换被声明成了implicit的时候,那么无论你是显式地还是隐式地进行转换,编译器都能通过。而如果你将它定义成explicit的时候,编译器仅仅允许显式转换。和其他的运算符一样,强制转换必须定义成public和static。

假如你知道某个强制转换总是安全的,无论初始变量的值是什么的时候,你可以将它定义为implicit。

假如你知道可能某些值的强制转换上可能会出现错误——譬如数据丢失或者别的异常——你需要将此强制定义成explicit。

【32】6.8.1 实现用户定义的类型强制转换

1.decimal比float更精确。

2.强制转换的语法对struct还是class来说都是一样的。

3.事实上如果你将uint转换成float的话,可能会有精度丢失,但是Microsoft认为这个误差是可以接受的,因此将uint转float也定义成implicit的。

4.然而,当你想把一个float值转换成uint的时候,就需要定义新的转换。float类型的值可以存储负数,但我们上面定义了Currency只允许存储非负数,并且float能存储的数值远大于Currency中uint类型的Dollars所能存储的数值。因此,假如一个float不是一个合适的值时,将它转换成Currency会导致不可预期的结果。因为存在这样的风险,将float转换成Currency必须声明成explicit的。(前提是隐式地)

5.显式地转换是可以的:

float amount = 45.63f;
Currency amount2 = (Currency)amount;

6.计算机存储数值的时候用的是二进制而非十进制,而在二进制中,像0.35这样的小数并不能精确地进行表示(就像1/3无法用十进制准确表示一样,它只能用0.3333…3来表示)。计算机实际上存储的是一个比0.35略微小一点点的值,这个值可以在二进制中准确表示。

7.可以用Convert.ToUInt16来解决上面那个问题。

8.为何预期的溢出异常没有被抛出?未被标记成checked。

9.System.Convert方法在它们内部自己实现了溢出检查。因此,实际上我们不需要将Convert.ToUInt16方法包含在checked代码段中。当然checked仍然是需要的,因为dollar部分的处理有可能溢出。

10.假如你定义的类型转换经常会被调用,那么在某些以性能为最优先的情况下,你可能更愿意不进行任何错误检查。这通常也是合情合理的,前提是你清楚描述了你的类型转换代码实现过程并且说明了该转换内部未实现任何错误检查。

【33】6.8.2 不同类之间的强制转换

1.假如一个类派生自另外一个类,你无法在这两者之间再次定义强制转换(因为它们之间的转换已经实现了)。

2.强制转换必须定义在互相转换的类型中,定义在它们俩中哪一个都行,但不能定义在其他第三者的类型中。

3.当你在某个类中定义了一种强制转换,你不能将它重复定义到另外一个类中。很显然,对于一种类型转换,仅能存在一份有效代码,否则编译器不知道应该调用谁。

【34】6.8.3 基类和派生类之间的强制转换

1.基类的引用,可以引用基类的对象实例,也可以引用它派生类中的对象实例。在面向对象编程中,派生类的实例,实际意义上,其实是基类的实例,只不过多了些额外的东西。所有基类中定义的成员,派生类中都有,派生类中拥有的内容,完全足够构造一个基类的实例。

2.假如你想将一个实际上是基类的实例转换成一个派生类的实例,你无法通过强制转换语法来实现。最好的方式是在派生类里定义一个构造函数,将基类实例作为参数传入,然后进行相应的初始化,如下所示:

class DerivedClass: BaseClass
{
public DerivedClass(BaseClass base)
{
// initialize object from the Base instance
}
}

【35】6.8.4 装箱和拆箱转换

1.从任何结构体(或者基础数据类型)到object之间的强制转换总是可行的并且是隐式地——因为它是从派生类转换到基类——并且它是一个装箱的过程。例如使用Currency结构体:

var balance = new Currency(40,0);
object baseCopy = balance;

当执行这个隐式转换的时候,balance的内容将会被拷贝到托管堆上,置于一个装箱对象(boxed object)里,而baseCopy则引用这个装箱对象。

2.拆箱。就像从一个基类引用转换成派生类引用一样,它必须是显式的声明,因为当被转换的实例不是正确的对象的时候会产生异常:

object derivedObject = new Currency(40,0);
object baseObject = new object();
Currency derivedCopy1 = (Currency)derivedObject; // OK
Currency derivedCopy2 = (Currency)baseObject; // Exception thrown

这段代码的结果跟上面我们讲的基类和派生类之间的很像。将derivedObject转换成Currency能生效是因为derivedObject实际上引用的就是一个装箱后的Currency实例——实际上的转换就是从装箱后的Currency类实例将所有值都拷贝到新的Currency结构体derivedCopy1中。而第二行的derivedCopy2转换失败了是因为baseObject本身指向的并非是一个装箱Currency实例。

3.当使用装箱和拆箱时,非常重要的一点是明白这俩过程实际上是对装箱对象或者拆箱后的结构体进行操作。因此,操作装箱后的对象,不会影响到原始的值类型。

【36】6.9 多级强制转换

1.当你尝试对两个类型之间进行转换,而C#编译器在处理时,发现两者之间没有直接的转换关系,它会尝试是否存在二次以上的转换能最终实现这个转换效果。(如:int转float转double)

2.100u的结果为uint类型。

3.当你将类型转换应用于一些带有多个重载的方法的时候,结果是不可预知的。

4.当你调用某个拥有多种重载版本的方法时,假如你传递的参数和所有版本的方法都不是精确匹配的话,实际上你要求编译器不但只处理参数类型的转换,还要求编译器选择使用何种版本的重载方法。编译器总是按照既定逻辑和严格的规则进行运行的,只不过结果可能并非像你预期的那样。假如存在任何有争议的情况,你最好还是显式地指定你想应用何种类型转换。

【37】小结

本章主要着眼于C#提供的标准运算符,描述了对象相等性之间的机制,并且考察了编译器是如何在标准数据类型之间进行转换的。演示了如何通过运算符重载,来为你自己定义的类型,实现各种运算。最后,学习了一种特殊的重载运算符,强制类型转换运算符(cast),它让你可以指定你自己的类型如何转换成另外一种数据类型。