.NET Book Zero 读书笔记(二)(从C++的使用者角度学习C#)

Methods and Fields

C#里把函数叫做method,所有的C#程序都需要有一个叫做Main的method。
在类里定义、但是又不在类内函数里定义的变量叫做field。

跟C++一样,函数的前面也有传值和传引用的方式,对于struct类型,写法如下:

// 这是两个函数
static void AlterInteger(int i){}
static void AlterInteger(ref int i){}

static void main()
{
	int i = 22;
    AlterInteger(i);
    AlterInteger(ref i);// 传参的时候也要带ref
}

而在C++里,这两种函数是无法区分的:

void func1(int a) { a++; }
void func1(int& a) { a++; }

int main() 
{
	int a = 0;
	func1(a);//Compile Error: More than one instance matches the argument list
}

说完了ref,再来说说out,当被编译到IL(intermediate language)时,ref和out的作用是一模一样的,但是在编译期间,二者作用略有不同。对于ref对应的引用,C#编译器(csc)要求该引用对应的对象已经被定义,而out则不需要该对象被定义,但是下面的代码还是会编译报错:

static void AlterInteger(out int i)
{
	i += 33;// 编译报错
}

函数中传递数组
这里的数组指的是C#的内置数组,不是List这种数组,函数传递数组时,会将其当做class类型传参,不过也有一些区别,代码如下所示:

class A{
// 传入的参数为数组的引用,这里对其进行了复制(a copy pf the reference),并没有复制整个array
static void func1(int[] arr)// 传递的是arr的引用的复制, 只能更改子元素
{
	arr[0] = 1;// 有效操作
	arr = new int [5];// 无效操作,这只是在改变引用,不影响原本的数组
}

// arr就是原本的数组
static void func2(ref int[] arr)// 传递的是arr引用本身,可以对arr本身进行修改
{
	arr[0] = 1;// 有效操作
	arr = new int[5];// 有效操作
}
};
static void Main()
{
	int[] arr1 = new int[10];
	int[] arr2 = new int[10];
	A.func1(arr1);
	A.func2(ref arr2);
	Console.WriteLine(arr1.Length);// 打印10, 没有改变原本的数组
	Console.WriteLine(arr2.Length);// 打印5, 改变了真正的数组
}

params关键字
先看这么一段代码:

class PassParamsArray
{
    static void Main()
    {
        int[] arr = { 22, 33, 55, 100, 10, 2 };
        Console.WriteLine(AddUpArray(arr));// 成功运行
        Console.WriteLine(AddUpArray(22, 33, 55, 100, 10, 2));//编译失败 
    }
   
    // 计算数组的总和
    static int AddUpArray(int[] arr)
    {
        int sum = 0;
        foreach (int i in arr)
            sum += i;
        return sum;
    }
}

而只要把AddUpArray里的参数改为params int[] arr,上面编译失败的地方就能成功运行。这种功能,感觉很像C++里的花括号的Initializer_list。C#里一个method的签名里最多一个参数被声明为params,而且该参数必须是签名里最后一个参数,C#里的库函数Console.WriteLineString.Format就用到了params的关键字


使用System.Diagnostics来诊断程序性能
用法很简单:

using System;
using System.Diagnostics;

class TestArrayInitialization
{
    const int iterations = 10000000;
    static int[] daysCumulativeDays = { 0, 31, 59, 90, 120, 151,
 181, 212, 243, 273, 304, 334 };

    public static void Main()
    {
        Stopwatch watch = new Stopwatch();
        Random rand = new Random();
        watch.Start();
        for (int i = 0; i < iterations; i++)
            DayOfYear1(rand.Next(1, 13), rand.Next(1, 29));
        watch.Stop();
        Console.WriteLine("Local array: " + watch.Elapsed);
        watch.Reset();
        watch.Start();
        for (int i = 0; i < iterations; i++)
            DayOfYear2(rand.Next(1, 13), rand.Next(1, 29));
        watch.Stop();
        Console.WriteLine("Static array: " + watch.Elapsed);
    }

    static int DayOfYear1(int month, int day)
    {
        int[] daysCumulative1 = { 0, 31, 59, 90, 120, 151,
 181, 212, 243, 273, 304, 334 };
        return daysCumulative1[month - 1] + day;
    }

    static int DayOfYear2(int month, int day)
    {
        {
            return daysCumulativeDays[month - 1] + day;
        }
    }
}

输出如下:

Local array: 00:00:02.2036147
Static array: 00:00:00.3249919


Exception Handling

C#支持structured exception handling,C#里的Exception发生于Runtime,这些Exception一般被Class识别出来,比如DivideByZeroException类,绝大多数基本的exception类都在System命名空间下。在很多methos、class和property的相关文档里,都会明确罗列出,这些内容可能会引起的Exception的类型。

举个例子,the static Double.Parse() method的文档里提到它会产生三种异常:

  • FormatException:参数格式错误
  • OverflowException:如果输入的值超过了Double能表示的区间
  • ArgumentNullException:string参数为Null

使用try catch可以自行处理可能抛出的异常,代码如下:

double input;
try
{
	input = Double.Parse(Console.ReadLine());
}
catch// 捕获所有可能出现的异常
{
	Console.WriteLine("You typed an invalid number");
	input = Double.NaN;
}

也可以针对特定异常进行捕获,与C++类似:

try
{
	// Statement or statements to try
}
catch (System.Exception exc)
{
	// Error processing
	Console.WriteLine(exc.Message);
	// you‘ll effectively call the ToString method of the Exception class
	// which displays detailed information, including a stack trace
	Console.WriteLine(exc);// 一种很有效的查问题的方法
}

还可以这么组合:

try
{
 input = Double.Parse(Console.ReadLine());
}
catch (FormatException exc)
{
 // Handle format exceptions
}
catch (OverflowException exc)
{
 // Handle overflow exceptions
}
catch (ArgumentNullException exc)
{
 // Handle null argument exceptions
}
catch (Exception exc)
{
 // Handle all other exceptions (if any)
}
// 注意, 这里的finally和前面的catch{}并不冲突 
finally
{
 // Statements in finally block
}

虽然这里的finally和前面的部分最后的catch都是为了在最后捕获所有没有罗列出来的异常,但是二者是有区别的。如果在前面的ctach对应的block里有return相关的语句,最后的finally的代码仍然可以执行(跟goto类似),所以一般finally是用来做Clean Up的。所以还会有这种写法:

try{
	// ...
}
finally{
	// the user is notified of the error as if the program did not handle the exception
	// but the finally clause is executed before the program is terminated.
	// 不管啥错, 都在finally进行执行和清理
	// ...
}

throw关键字
有两种写法:

// 写法一: throw;
try
{

}
catch (Exception)
{
    throw;// 单独的thorw语句只可以在Catch Block里放置,用于rethrow the exception
}

// 写法二: throw new Exception();
if (strInput == null)
 throw new ArgumentNullException();// 这些异常类都是Exception的派生类
 // 或者
 throw new ArgumentNullException("Input string");// string代表参数名称

这里我还开了个exe,去看看啥情况,确实是会报错,注意开exe的时候要开.net frame work工程。。。否则只能build出来dll,得不到exe文件:
在这里插入图片描述


Class、Structs and Objects

两个问题:
C#里是不是只要看到new,就是会在堆上分配内存?
不是的,比如说Vector3 vec = new Vector3();,Vector3是struct类型,这里的new仅仅是调用了默认构造函数,它对Vector3对象进行了bitwise的初始化,都置为0,本身并没有在堆上做任何处理


C#的数组是不是都是在heap上分配的,那么struct数组和class数组有何区别?
如果是struct,那么堆上会直接分配好对应的内存大小,每一个对象默认都是0的,如果是class,则会在堆上分配多个引用,这些引用都是null,因为还未被初始化,每个元素还要调用new为其初始化。而两种类型的数组应该都会在stack上保留一个数组的引用

另外两个Tips:

  • 使用new可以对class和struct创建实例,实例里的值类型都会被置为0,引用类型都是null
  • You can‘t use foreach to call new to create elements in arrary because the array elements are readonly in the body of the foreach.


Instance Methods

  • 如果一个类不override ToString函数的话,那么打印它会输出它的类名
  • .NET Framework下所有的Class和Struct都有一个ToString函数,大概代码如下:
public virtual string ToString()
{
 return GetType().FullName;// GetTyoe也是System.Object下的函数
}
  • 结构体不可以被其他的任何东西继承,而且结构体不可以显式的继承于任何东西(这意味着可以隐式的继承)A structure cannot explicitly inherit from anything else, and nothing can inherit from a structure.
  • C#里所有的class和struct都继承于System.Object,所有的struc都隐式的继承于System.ValueType,而后者直接继承于System.Object(后面还会细讲)


Constructors

  1. 如果类或者结构体里的field都没有初始值,那么使用默认的new创建对象,对象的值类型成员都会变为0,引用类型成员都会变为Null,但是如果有初始值就不一样了:
// Note: you can initialize fields only for a class
class A
{
    public int index = 1;// 在new A()得到的对象的index不再是0, 而是1
}

struct B
{
    public int index = 1;//编译错误, cannot have instance property or field initializers in struct
}// 这也是struct消耗小的原因,创建对象时内存上全部是0,不允许自定义的值
  1. 创建struct的数组比创建class的数组快的多,消耗也小得多,原因上面提到了,因为不用初始化
  2. 问题,struct里是不是只能是值类型,如果有引用类型怎么办? 答案是可以,struct里还可以定义class,初始化时该类型被置为null
  3. 调用构造函数时,下面这段代码的执行结果?
class Date {
int year;
int month = 4; int day = 1;
           public Date()
           {
year = 1; month = 1; ...
} ...
}

这段代码,编译器会先为field初始化,month等于4 然后调用ctor 所以最后month等于1

  1. C#和C++的ctor里都可以调用instance method,但是要小心,而且C++的ctor里不可以调用虚函数
  2. C#里类的ctor可以调用该类的其他的ctor,写法如下:
Date dateNewYearsDay = new Date(2008);

public Date(int year) : this(year, 1, 1)// 这种叫做ctor initializer
{
}

struct不允许定义默认的构造函数
C#也是,在Class里,当不提供任何ctor时 编译器会自动生成默认的,注意这里说的是类,对于结构体,结构体永远会有无参的构造函数,而且struct不允许定义默认的构造函数

 struct Vec1 
 {
    public Vec1()// 编译错误, Structs cannot contain explicit parameterless constructors
    {
        a = -1;
    }

    public Vec1(int c)//没问题
    {
        a = 1;
    }


    private int a;
}

class Vec2 
{
    public Vec2()//没问题
    {
        a = 1;
    }

    private int a;
}

什么是Static Ctor
C#的类和结构体都可以定义一个static ctor,这个函数必须没有参数,在C#里,类的static成员都是用这个函数去初始化的,代码如下:

class Data{
	// 此函数会在static期间执行,优先于任何instance的ctor执行,也优先于类内的static成员的初始化
	static Date()// 而且这个函数不能声明为public, 只能用这种参数的写法
	{
	 ...
	}
}

From MSDN:

A static constructor is used to initialize any static data, or to perform a particular action that needs to be performed once only. It is called automatically before the first instance is created or any static members are referenced


Static constructors的特点
Static constructors have the following properties:

  • A static constructor does not take access modifiers or have parameters. static ctor不可以有参数,也不可以有access modifiler
  • A static constructor is called automatically to initialize the class before the first instance is created or any static members are referenced.
    static ctor. 在该类的第一个实例被创建,或者static成员被使用时,会被调用
  • A static constructor cannot be called directly. 不可以直接调用static ctor
  • The user has no control on when the static constructor is executed in the program. User无法控制static ctor的调用时间
  • A typical use of static constructors is when the class is using a log file and the constructor is used to write entries to this file. static ctor经常被用于使用log文件的类,在里面可以对文件写入entries
  • Static constructors are also useful when creating wrapper classes for unmanaged code, when the constructor can call the LoadLibrary method. Static ctor也可以用于为unmanaged代码创建wrapper classes,在里面可以调用LoadLibrary函数


Concepts of Equal

Equal函数
System.Object有一个虚函数:

public virtual bool Equals(object obj)

可以先看这么一段代码:

static void Main()
{
	PointStruct ps1 = new PointStruct();
	ps1.x = ps1.y = 55;
	PointStruct ps2 = new PointStruct();
	ps2.x = ps2.y = 55;
	PointClass pc1 = new PointClass();
	pc1.x = pc1.y = 55;
	PointClass pc2 = new PointClass();
	pc2.x = pc2.y = 55;
	Console.WriteLine("ps1.Equals(ps2) results in " + ps1.Equals(ps2));// 两个struct的比较
	Console.WriteLine("ps1.Equals(pc1) results in " + ps1.Equals(pc1));// class与struct的比较
	Console.WriteLine("pc1.Equals(pc2) results in " + pc1.Equals(pc2));// 两个class的比较
	Console.WriteLine("pc1 == pc2 results in " + (pc1 == pc2));// 直接比较两个class
	Console.WriteLine("ps1 == ps2 results in " + (ps1 == ps2));// 直接比较两个struct, 这一行会编译报错
}

注释掉最后一行,输出结果是:

ps1.Equals(ps2) results in True
ps1.Equals(pc1) results in False
pc1.Equals(pc2) results in False
pc1 == pc2 results in False

Class的Equals函数默认是比较两个reference,所以都是false,而所有的Structs都有一个Implicit Base Class,叫做ValueType类,它overeride了Equals函数,当两个struct类型相同时,C#里的Struct的Equals函数本质上是执行a bitwise equality test

还有个特殊的地方,C#里的struct是不允许通过==或者!=进行判断的,这种操作会编译报错,后面会介绍允许struct进行==操作的做法,应该是类似C++的运算符重载。


函数参数ref class 和class的区别
在C#的层面理解,传入class对象作为函数的参数时,它是传入了引用的复制,如果更改引用对象本身是不会生效的,比如obj = nullobj = new ClassType()这种操作都不会生效,但传入ref obj就是传入引用本身了,此时改动任何内容就是改变原对象。

从C++的层面理解:传入class对象作为函数参数时,可以认为这里创建了一个指针,指向该对象,然后作为参数传入该指针,此时就算对该指针置为null,也不会影响原本物体在内存上的分配。而使用ref时,相当于直接传入对象的引用,也就是相当于传入对象原本的地址(或者说对应的指针),此时就可以直接对该对象进行操作。

看段代码:

class PointClass
{
	public int x, y;
}

static void Main()
{
	PointClass pc1 = new PointClass();
	pc1.x = pc1.y = 22;
	Console.WriteLine("Before method: pc is ({0}, {1})", pc1.x, pc1.y);// 22 22
	ChangeClass(pc1);
	Console.WriteLine("After method: pc is ({0}, {1})", pc1.x, pc1.y);// 33 33
	
	PointClass pc2 = new PointClass();
	pc2.x = pc2.y = 22;
	Console.WriteLine("Before method: pc is ({0}, {1})", pc2.x, pc2.y);// 22 22
	ChangeClass(ref pc2);
	Console.WriteLine("After method: pc is ({0}, {1})", pc2.x, pc2.y);}

static void ChangeClass(PointClass pc)// 传入的pc是一个复制的引用(理解为指针更好)
{
	pc.x = pc.y = 33;
	pc = null;// 其实没有作用
}

static void ChangeClass(ref PointClass pc)// 传入的pc是原本的地址
{
	pc.x = pc.y = 33;
	pc = null;
}

输出:

Before method: pc is (22, 22)
After method: pc is (33, 33)
Before method: pc is (22, 22)
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at CSharpTest1.Program.Main() in C:\Users\tuoba\source\repos\CSharpTest1\CSharpTest1\Program.cs:line 31

这一点感觉还挺重要的。



Fields and Properties

学习这章之前,提几个问题:

  • 是不是Fields都是private的,Property都是public的
  • Field和Property的区别到底是什么,目前我只知道Property exposes Field,那么public int a是不是就是Property,private int b是不是就是field
  • 随着C#的版本不断升级,现在的public int A;是不是等同于下面这段代码:
public int A
{
	get
	{
		return _A;
	}
	set//注意写法, set也不要参数
	{
		_A = value;
	}
}
private int _A;
  • Property和Field有什么命名规则吗

随着.NET Framework的发展,XML变得越来越重要,这使得Properies也变得越来越重要了。为啥这么说呢?这是因为在一些编程接口,比如说WPF(Windows Presentation Foundation),程序员们开始使用XAML(XML-based Extensible Application Markup Language)来定义窗口的UI布局了。

比如说这是一段WPF代码:

Button btn = new Button();// 注意: Ctor是没有参数的
btn.Foreground = Brushes.LightSeaGreen;// 后续的参数都在调用无参的构造函数后执行
btn.FontSize = 32;
btn.Content = "Hello, WPF!";

上面这段代码,可以轻松的翻译为下面的XAML语句:

<Button Foreground="LightSeaGreen" FontSize="32"
 Content="Hello, WPF!" />

This translation between code and XAML is so easy and straightforward primarily because Button has a parameterless constructor and defines properties to set the color, font size, and content. Imagine the translation mess if Button had only a parametered constructor, and if it had various methods rather than properties to set its characteristics.

readonly
readonly关键字只可以用在fileds上,它可以让fields对应的对象变为只读的,也就是说该对象只可以在被创建的时候设置初始值,之后就不可以进行更改了,也就是说,readonly对应的field只能在Ctor里进行初始化。代码如下所示:

class Date
{
	public readonly int year, month day;
	public Date(int year, int month, int day)
	{
		this.year = year;
		...
	}
 	...
}

也就是说,如果一个类里有field是readonly的,那么这个类的对象被创建后,该对象就是immutable的


const
前面提到的readonly是适用于fields上的关键字,所以说它是用于修饰类的Instance的,而const可以修饰两种类型:

  • 类里的fields
  • 函数里的变量
    const定义的变量,其值必须在编译期决定,const自带static属性,比如Math.PI就是public constant field

const和static readonly
既然const自带static属性,那么它跟static readonly的作用是非常类似的,在读书笔记(一)里其实提到过:

C#中,const是在编译时期就进行了替换,而static readonly是运行时的,二者基本差不多

具体有什么区别,可以看这么个例子:

const Date dateSputnik = new Date(1957, 10, 4); // Won’t work
// 注意,下面这行代码必须在类内,因为必须是field
static readonly Date dateSputnik = new Date(1957, 10, 4); // Works fine

这是因为const是在编译期替换的,而C#里的new expression是一个runtime才会被执行的东西,所以失败了,使用static readonly相当于C++里面声明一个常量引用,只是不可以通过dataSputnik改变右边的东西而已,等号右边的对象自己还是可以改的(如果没有readonly field的话)


五种Access Modifiers
C++有三种,但C#有五种:

  • public
  • private
  • protected
  • internal
  • internal protected

Properties
在C++里,一个private变量,如果允许外部读写的话,往往会写一个Get和Set函数,C#通过Property的方式简化了这个过程,与methos、fields和ctor一样,Properties也是类的成员。properties也经常被叫做"smart" fields

有以下特点:

  • Property一般首字母大写
  • Property的使用方法跟Field几乎一模一样
  • 很多Property背后都有一个private field(backing field),但也不是必须的
  • Property一般都是Public的
  • Property可以是static的,Fields也可以是static的

编译器如何区分Property、Field和Method
看看Property的代码:

public int Year
{
	set//这玩意儿叫accessors
	{
		if (value < 1)
			throw new ArgumentOutOfRangeException("Year");
			year = value;
	}
	get
	{
		return year;
	}
}

编译器会看public int Year,后面的内容,如果跟的是;=,,那么就是field,如果跟的是花括号{,那么就是Property,如果跟了个(,那么说明是函数

Property的本质
Property实际上不是CLS(Common Language Specification)里的东西,IL里也没这玩意儿,实际上是C#编译器为Property自行创建的set_Property和get_Property函数,所以,其实Property里的set和get,根本不是C#的关键字,在外面可以用get和set作为变量的名字。所以如果自己定义一个对应名字的函数,会报错,如下图所示,挺有意思的:
在这里插入图片描述

一个规范格式的Data类

using System;

class Date
{
    // Private fields
    int year = 1;
    int month = 1;
    int day = 1;
  
    // Public properties
    public int Year
    {
        set
        {
            if (!IsConsistent(value, Month, Day))
                throw new ArgumentOutOfRangeException("Year");
            year = value;
        }
        get
        {
            return year;
        }
    }
    public int Month
    {
        set
        {
            if (!IsConsistent(Year, value, Day))
                throw new ArgumentOutOfRangeException("Month = " + value);
            month = value;
        }
        get
        {
            return month;
        }
    }
    public int Day
    {
        set
        {
            if (!IsConsistent(Year, Month, value))
                throw new ArgumentOutOfRangeException("Day");
            day = value;
        }
        get
        {
            return day;
        }
    }// Parameterless constructor
    public Date()
    {
    }
    // Parametered constructor
    public Date(int year, int month, int day)
    {
        Year = year;// Ctor直接与Property打交道
        Month = month;
        Day = day;
    }
    // Private method used by the properties
    static bool IsConsistent(int year, int month, int day)
    {
        if (year < 1)
            return false;
        if (month < 1 || month > 12)
            return false;
        if (day < 1 || day > 31)
            return false;
        if (day == 31 && (month == 4 || month == 6 ||
        month == 9 || month == 11))
            return false;
        if (month == 2 && day > 29)
            return false;
        if (month == 2 && day == 29 && !IsLeapYear(year))
            return false;
        return true;
    }
    // Public properties
    public static bool IsLeapYear(int year)
    {
        return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
    }
    static int[] daysCumulative = { 0, 31, 59, 90, 120, 151,
 181, 212, 243, 273, 304, 334 };
    public int DayOfYear()
    {
        return daysCumulative[Month - 1] + Day +
        (Month > 2 && IsLeapYear(Year) ? 1 : 0);
    }
    static string[] strMonths = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
    public override string ToString()
    {
        return String.Format("{0} {1} {2}", Day,
        strMonths[Month - 1], Year);
    }
}

定义类自己的下标操作符
相当于C++里的重载运算符[],写法如下:

// 实现Date类的下标索引操作: 分别返回年、月、日
// 函数名叫this, 后面跟的不是(), 是[]
public int this[int i]
{
    get
    {
        switch (i)
        {
            case 0: return Year;
            case 1: return Month;
            case 2: return Day;
            default: throw ArgumentOutOfRangeException("index = " + i);
        }
    }
}

C#Indexer的本质
与Property相同,都是Compiler Trick,声明上面是一个this[]的时候,编译器会自己生成一个Property,名字叫做Item,由于Item是Property,所以编译器会随之生成get_Property函数和set_Property函数,所以这里的Index的本质,就是调用对应的Set和Get函数,如下图所示:

在这里插入图片描述


回答前面的问题

  1. fields可以是public,也可以是private,Properties也是如此,但是一般的编程规范要求,Property都为public,Field都为private
  2. field是单纯的数据,而property本质是Get和Set函数,一般是由编译器为field生成的
  3. public int A,应该是field
  4. Property一般首字母大写,而Field一般首字母小写或者为下划线

Programmers experienced in .NET have pretty much come to the conclusion that instance fields should always be private, and that public
access to these private fields should be through public properties that guard against invalid values. Public properties always have capitalized names; private fields often have names that begin with lowercase letters, or perhaps underscores.



Inheritance

class可以继承,但是struct不行,struct不可以继承于struct或class,class也不允许继承于struct

所有的struct都隐式继承于System.ValueType,而System.ValueType是一个abstract class,继承于System.Object。

设计系统的架构是一门艺术,如下所示是Windows上WPF的部分架构:
在这里插入图片描述
补充继承关系:

Control : FrameworkElement : UIElement : Visual : DependencyObject : DispatcherObject : Object.

注意,C#不支持多重继承,这意味着每个类只能继承一个类

派生类调用基类的Ctor

class ExtendedDate : Date
{
    public ExtendedDate()
    {
    }
    public ExtendedDate(int year, int month, int day) :
    base(year, month, day)
    {
    }
}

** Constructors are not inherited.**
C#里的派生类可以继承非private的Method、Property和Field,但是不可以继承Ctor,只可以通过Base去调用对应的Ctor,这一点跟C++是类似的,派生类无法在Ctor里阻止基类、以及基类的基类的构造函数的调用(C#应该是不行,C++好像也不行)。

Protected
与C++一样,C#的protected成员也是不允许外部访问,但是允许子类访问。



Virtuality

  • C#里的派生类是可以隐式转换为基类的,有以下两个原因:
  1. 它们都是引用,而C#里所有引用的大小是相同的,所以在内存,也就是栈上占的空间也相同
  2. 逻辑上是可以的,因为继承是is-a关系
  • C#类里的Property和Method都可以是virtual的,但是Field不行

Class的对象的类型信息会被存在heap上
比如说有个class T,现在有这么一行语句:

T t = new T();
object obj = t;
// 仍然会调用T类型的ToString函数,而不是Object类型的
Console.WriteLine(obj);

declared type and actual type
C++里类型有static和dynamic两种,C#也有两种,只是叫法不一样

向下转型的两种写法
基类向派生类转型有两种写法:

Base b = new Base();
// 写法一,如果转型失败会抛出异常
Derived d = (Derived)b;
// 写法二,如果转型失败会返回null
Derived d = b as Derived;

获取Object的类型
System.Object里有一个GetType函数,所以C#里所有的对象都可以获取其Type

// 如果obj为null则会抛出异常
Type typeObjVariable = obj.GetType();
Type typeExtendedDateClass = typeof(ExtendedDate);// 根据类名获取Type

这里的Type尽管是class类型,但是每一个类,都只有一个对应该类类型的对象,这样才能方便的用GetType(T) == type类似的语句,把它当Struct用,使用==进行比对:

//  A Type object that represents a type is unique
// 虽然两个type对象是不两个类对象的引用,但是由于它们指向相同的Unique object,所以可以用==来判断相等
// 这里的GetType是instance method, 而typeof是C#的operator
obj.GetType() == typeof(Date)

注意,基类和派生类的is-a关系在Type面前不管用:

derived.GetType() == typeof(Base);// returns false

is关键字
除了as,还可以用is判断对象是不是某种类型,is的逻辑是,只要左边的对象可以被转换到右边的类型,那么就返回true:

// 注意, 即使obj是null也不会抛出异常, 只会返回false
obj is MyClass// returns bool
// is运算符
derived is Base;// 继承本就是is-a关系,所以返回true

所以说,可以用asis来避免抛出异常,还可以用is来判断对象是否实现了对应的interface

// 比如说,IEnumerable接口,只有实现了这个接口的类才可以执行foreach语句
obj is IEnumerable;

在派生类中使用new关键字忽略掉继承来的Method或Property
代码如下:

class BaseClass
{
    public virtual void VirtualMethod()
    {
        Console.WriteLine("VirtualMethod in BaseClass");
    }
    public void NonVirtualMethod()
    {
        Console.WriteLine("NonVirtualMethod in BaseClass");
    }
}
class DerivedClass : BaseClass
{
    public override void VirtualMethod()
    {
        Console.WriteLine("VirtualMethod in DerivedClass");
    }
    public new void NonVirtualMethod()// 使用new来忽略继承来的函数
    {
        Console.WriteLine("NonVirtualMethod in DerivedClass");
    }
}

这个new也可以不写,不过编译器会给一个warning,派生类里使用new其实有这么几个用处:

  • 改变继承过来的函数的返回类型,毕竟函数重载是不考虑返回类型的
  • 把继承来的东西从protected变为Public(应该是重写一个函数,然后里面调用base的函数?)
  • 单纯的改变函数的内容,比如上面的例子

C#里的纯虚函数与接口类
C++里是通过纯虚函数来实现接口的,一个类只要有纯虚函数,就是抽象类,而且要想创建对应的对象,子类必须实现纯虚函数。
但C#里分成了两个概念,抽象类和抽象接口函数,代码如下所示:

// 一个类可以是抽象类, 即使里面啥接口函数都没
public abstract class A
{
}

public abstract class B
{
	// 所有B的子类必须实现这个函数才可实例化, 因为它没有function body
	public abstract decimal MyMethod();// abstract函数也是虚函数
}

Sealed Class
也就是不允许被继承的Class,内容大部分是Static 成员(Fields、Properties和Methods)的类一般都是Sealed Class,比如Math、Console、Convert等,注意,Structures are also implicitly sealed.

此处在仔细想想为什么struct不可以被继承,对于以下代码:

baseObj = derivedObj;

如果两个都是struct,那么二者的内存所占大小很可能是不同的,不同内存的东西赋值是不允许的,而Class就不一样,因为它们在栈上都是引用,所以都是相同的大小,可以进行赋值

装箱与拆箱
当struct被用于给class赋值时,该class对象会在堆上分配对应的内存,用于拷贝struct对应的数据:

decimal pi = 3.14159m;
object obj = pi;// Boxing

反过来叫拆箱,堆上的内容会被复制到栈上:

decimal m = (decimal) obj;


Operator Overloading

可以重载的运算符
C#里也是可以重载运算符的,分为一元运算符、二元运算符。
可以重载的一元运算符有:

  • +-代表正负的一元运算符
  • ~
  • ++
  • --
  • 布尔运算符:bool和false

二元运算符有

  • +-*/,也就是加减乘除
  • %
  • &|^,逻辑与位运算符
  • ==!=,equality运算符
  • <<=>>=,relational运算符
  • <<>>,shift运算符
  • +=-=等compound运算符应该是使用了各自对应的二元运算符的实现

运算符重载的写法
C#里的运算符重载写法要注意以下几点:

  • 都是public static函数,写法是public static operator ReturnType ...
  • 好像在运算符重载对应的函数里不允许改变传进来的Object的值,我看都是new了一个新的对象并作为return type返回
  • 有些运算符之间有一些联系和特殊的规定,比如重载了==运算符,就必须重载!=运算符,后面还会具体提到。
  • 运算符重载与Property、Indexer一样,本质都是编译器做的Trick,不属于CLS里的内容,比如编译器会为-的运算符重载生成一个叫op_Subtraction的函数

举个例子,比如说有两个日期,现在定义一个减号运算符,让其可以相减,算出两个日期相差的天数:

public static int operator - (SuperDate sdLeft, SuperDate sdRight)
{
 	return sdLeft.CommonEraDay - sdRight.CommonEraDay;;
}

由于编译器为加减乘除运算符重载生成的函数名很不通用,为了方便其他的脚本语言的调用,一般定义这些运算符之后,还会定义对应的Add、Subtract、Divide和Mutiply函数,代码如下所示:

// 最核心的版本
public static SuperDate Add(SuperDate sdLeft, int daysRight)
{
	return new SuperDate(sdLeft.CommonEraDay + daysRight);
}

// 间接调用的版本
public static SuperDate operator + (SuperDate sdLeft, int daysRight)
{
	return Add(sdLeft, daysRight);
}

运算符重载的交换性
C++里的类与int的加减应该是必须类在加号的左边的,但是C#里可以定义两个版本的+的运算符重载实现加法的交换性,代码如下:

// 还有上面提到的两个版本的加法函数
...

// 间接调用的版本, 但是参数顺序交换了
public static SuperDate Add(int daysLeft, SuperDate sdRight)
{
	return Add(sdRight, daysLeft);
}

// // 间接调用的版本, 但是参数顺序交换了
public static SuperDate operator + (int daysLeft, SuperDate sdRight)
{
	return Add(daysLeft, sdRight);
}

当重载==运算符时,别忘了继承于Object的Equals函数
每个对象都默认有Equals函数,默认是class对象比较引用,而object对象进行bitwise的比较,如果只重载了==和!=,没有override对应的Equals和GetHashCode函数,编译器会给予一个警告,如下图所示:
在这里插入图片描述
关于Equals函数,要注意一个事情:

The documentation of the Equals method in the System.Object class indicates that the method must not raise an exception

在实现Equals函数时,别忘了实现两个版本的Equals函数,一个是Instanc Method,一个是Static Method,如下图所示:
在这里插入图片描述


先来实现Instance Method,这里作者实现了自己的表示Date的类的Equals函数,代码如下:

//=================== 当obj为class时 ======================
// 注意,Equals函数里面是不允许抛出异常的,
public override bool Equals(object obj)
{
	if (obj == null) || GetType() != obj.GetType())
		return false;
	SuperDate sd = (SuperDate) obj;
	return CommonEraDay == sd.CommonEraDay;
}

// 也可以这么写
public override bool Equals(object obj)
{
	return obj is SuperDate && this == (SuperDate) obj;
}

//=================== 当obj为struct时 ======================
public override bool Equals(SuperData sd)
{
	return this == sd;
}

再来实现Static Method,代码如下:

public static bool Equals(SuperDate sd1, SuperDate sd2)
{
	return sd1 == sd2;
}

还剩下一个GetHashCode函数了,这个函数是程序用于帮助存储和获取对象的函数,它会返回一个int,当Equals函数判定两个对象相等时,该对象返回的HashCode也应该是一样的,这里的GetHashCode函数设计为:

public override int GetHashCode()
{
	return CommonEraDay;
}

explicit关键字
explicit的核心涉及到了,在类A中,如何定义对象a向对象b类型的隐式转换和对象b向对象a的隐式转换(假设a是A的对象,b是类B的对象)。

C++里两种转换的方式的写法是不同的:

  • 如果要让类A,接受B的对象向A的隐式转换,那么要申明一个A(const B& b)的构造函数
  • 如果要让类A,接受A的对象可以向B隐式转换,那么要申明一个运算符重载函数,代码如下:
class SimpleClass {
   // ...
   operator int() const;
};

SimpleClass::operator int() const
{
	...// 定义自己的转换成int的方式
	return m_int;
}

C++里通过explicit关键字,对只有一个参数的构造函数进行修饰,可以禁止该参数对应的对象被隐式转换为类对象。

但是在C#里,两种转换都是通过类的运算符重载的,代码如下所示:

// int不可以被隐式转换为SuperDate, 感觉这个函数类似于一个构造函数
// 如果不要explicit, 会编译报错
public static explicit operator SuperDate(int dayCommonEra)
{
	return new SuperDate(dayCommonEra);
}

// SuperDate对象不可以被隐式转换为int对象
public static explicit operator int (SuperDate sd)
{
	return sd.CommonEraDay;
}

implicit
同explicit类似,operator前面加implicit就是可以隐式转换了:

// 写起来真简单啊
public static implicit operator SuperDate(int dayCommonEra)
{
	return new SuperDate(dayCommonEra);
}

partial
使用partial可以在多个文件里定义类,比较常见的是在不同的接口里实现同一个的类,但是类是partial的

posted @ 2021-07-28 00:04  弹吉他的小刘鸭  阅读(53)  评论(0编辑  收藏  举报