C#读书笔记

此书为《C#入门经典(C#6.0&Visual Studio 2015)(第七版)》,购于2016年底
购买此书是因为工作需要接触部分C#代码,而那时我对面向对象编程的概念认识尚浅,工作接触的代码又比较庞大,于是寄希望于此书,希望能快速上手C#,了解基本的知识,避免闹笑话。
刚拿到这本书时,我急于求成,加上书前半部分的内容也很入门(读书时跳过了),约2、3周便利用闲暇时间读完了,这份读书笔记是今年6月左右开始整理的,希望能像当年学习高数一样仔细揣摩一下,但是仍有一些部分不求甚解(或者得了解确因为没有实例学习印象不深)。现在把书中的内容做个简单的总结,时常复习,未来学习软件架构时,作为工具也好有个参考。

第一章 C#简介

1.1 .NET Framework的含义

.NET Framework 是Microsoft为开发应用程序而创建的一个具有革命意义的平台。可运行于各种Windows\Linux版本和Mac OS,另外,一些版本可运行在iPhone和Android智能手机上。可以使用.NET Framework创建桌面程序、Windows Store应用程序、云/Web应用程序、Web API和其他各种类型的应用程序。.NET Framework的设计方式确保它可以用于各种语言,包括C#、C++、VB、JS甚至一些旧语言,所用这些语言都可以访问.NET Framework,他们彼此之间还可以通信。

1.2 .NET Framework 的内容

.NET Framework主要包含一个庞大的代码库,可以在客户语言(如C#)中通过面向对象编程技术(OPP)来使用这些代码。这个库包括多个不同的模块,这样就可以根据需要得到的结果来选择使用各个部分。其目的是,不同操作系统可以根据各自的特性,支持其中的部分或全部模块。部分.NET Framework库定义了一些基本类型。类型是数据的一种表达方式,制定最基本的类型有助于使用.NET Framework的各种语言之间进行交互操作,这称为通用类型系统(Common Type System,CTS)。
除提供这个库外,.NET Framework还包含.NET公共语言运行库(Common Language Runtime,CLR),它负责管理用.NET库开发的所有用用程序的运行。

1.2.1 使用.NET Framework编写应用程序

使用.NET Framework编写应用程序,就是使用.NET代码库编写代码(使用支持Framework的任何一种语言)。本书用VS进行开发,VS是一种强大的集成开发环境,支持C#(以及托管和非托管C++、VB和其他一些语言)。这个环境便于把.NET功能集成到代码中。我们创建的代码完全是C#代码,但使用了.NET Framework,并在需要时利用了VS中的其他工具。
为执行C#代码,必须把他们转化为目标操作系统识别的语言,即本机代码(native code)。这种转换称为编译代码,由编译器执行。但在.NET Framework下,此过程包含两个阶段。

1. CIL和JIT

在编译.NET Framework库的代码时,不是立即创建专用操作系统的本机代码,而是把代码编译为中间通用语言(Common Intermediate Language,CIL)代码,这些代码并非专门用于任何一种操作系统,也非专用于C#。其他.NET语言也会在第一阶段编译为这种语言。开发C#应用程序时,这个编译步骤由VS完成。
Just-In-Time(JIT)把CIL编译为专用于OS和目标机器结构的本机代码。这样OS才能执行应用程序。这里编译器的名称JIT反映了CIL代码仅在需要时才编译的事实。这种编译可以在应用程序的运行过程中动态发生,不过开发人员一般不需要关心这个过程。除非编写功能十分关键的代码,否则知道这个编译过程会在后台自动运行,不需要人工干预就可以了。

2. 程序集

编译应用程序时,所创建的CIL代码储存在一个程序集中。程序集包括可执行的应用程序文件(可直接在Windows中运行,不需要其他文件,扩展名为.exe)和其他应用程序使用的库(其扩展名是.dll)。
除包含CIL代码外,程序集还包含元信息(即程序集中包含的数据信息)和可选资源(CIL使用的其他数据,例如声音和图片文件)。元信息允许程序集是完全自描述的。当然,不必把运行应用程序所需的所有信息都安装到一个地方。可以编写一些代码来执行多个应用程序所要求的任务,此时,可以把这些可重用的代码放在所有应用程序都可以访问的地方。在.NET Framework中,这个地方是全局程序集缓存(Global Assembly Cache,GAC),把代码放在这个缓存中是很简单的,只需把包含代码的程序集放在包含该缓存的目录中即可。

3. 托管代码

在将代码编译为CIL,再用JIT编译器编译为本机代码后,CLR的任务尚未完成,还需要管理正在执行的用.NET Framework编写的代码(这个执行代码的阶段通常称为运行时(runtime))。即CLR管理着应用程序,其方式是管理内存、处理安全性以及允许进行跨语言调试等。相反,不受CLR控制运行的应用程序属于非托管类型,某些语言(如C++)可以用于编写此类应用程序,例如访问操作系统的底层功能。但C#只能编写在托管环境中运行的代码。我们将使用CLR的托管功能,让.NET处理与操作系统的任何交互。

4. 垃圾回收

托管代码最重要的一个功能是垃圾回收(garbage collection)。
第二章 变量和表达式

第二、三章 编写C#程序

原书本章内容实在太简单,目的是让编程小白写“Hello World”,不堪在此一提,这里直接和原书的第三章合并。

2.1 C#的基本语法

在VS中Tools|Options中可更改默认的代码样式。在注释符号///后面的文本可以在编译时提取,用于创建XML文档(必须遵循XML文档规则)

2.2 C#控制台应用的基本结构

使用#region和#endregion关键字来定义可以展开和折叠的代码区域(以#开头的任意关键字实际上是一个预处理指令,严格来说并不是C#关键字)

2.3 变量

使用标准类型可以在不同语言之间交互操作(.Net Framework)。有时候一些字面值有多种可能类型,如100可能为long,可能为int,也可能为double,可以通过后缀,并根据上下文判断其类型:

类型 后缀 实例
float f或F 1.5f
double 无\d\D 1.5
uint\ulongstripes u或U 100U
long\ulong l或L 100L

字符串的字面值中特殊符号可用转义符代替,也可在字符串的双引号之前加一个@字符,避免结束字符串,如@“Beijing‘s string.”
注意字符串本质上是引用类型,而非值类型,因此可赋值null,表示字符串变量不引用字符串。
注意变量声明后最好紧跟着变量初始化,原因是声明时不会为分配内存空间,初始化时会

int i;
string test;
for(i=0;i<10;i++)
{
    test="Line"+Convert.ToString(i);
    WriteLine($"{test}");
}
WriteLine($"{test}");//这里出错了,原因是test未初始化

2.4 表达式

默认情况下,C#代码包含在全局名称空间中。使用namespace关键字为花括号中的代码块显式定义名称空间。如果一个名称空间中的代码需要使用在另一个名称空间中定义的名称,就必须包含对该名称空间的引用,不同名称空间级别之间用句点(.)连接。
特别注意:using语句本身不能访问另一个名称空间中的名称,除非名称空间中的代码以某种方式链接到项目上,或者代码是在该项目的源文件中定义的。using语句仅仅是为了我们便于访问这些名称。

2.5 名称空间

使用关键字using添加名称空间,在C#6中新增关键字using static,允许把静态成员直接包含到C#程序的作用域中,例如可使用该关键字将System.Console静态类中的WriteLine()静态方法包含到作用域中:

using static System.Console.WriteLine;

这样,访问WriteLine方法时就不加必限定符号了。

第四章 流程控制

使用三元运算符< test >?< If True> : < If False>使代码更整洁。
在switch语句中每个case必须有break; 在最后要用default: 执行不属于以上任一case的情况。也可以对case进行叠加处理,例如:

switch ()
{
    case :
    case :
            …
            break;
    case :
            …
            break; 
    default:
            …
            break;
}

第五章 变量的更多内容

5.1 强制转换

强制转换时注意判断溢出,例如将double转化为short时,

double A=1000000;
unsigned char B;
B=checked((unsigned char)A);

以上代码在运行时会崩溃,并提示错误信息,(疑问,这个功能有什么意义呢,不能抛出异常吗)
也可以在编译时自动为所有类似表达式添加默认检查,设置方法为:在solution property中打开build设置,在advanced选项卡中激活Check for arithmetic overflow/underflow。

5.2 枚举

用关键字enum声明枚举

enum orientation:byte
{
    north=1,
    south=2,
    east=3,
    west=4
}
orientation myDirection=orientation.east;

5.3 结构

使用关键字struct声明结构

struct route
{
    public orientation direction;
    public double distance;
}

5.4 数组

记录几个实用的数组声明/初始化小例子:

int[] myArray={5,9,10,99};
int[] mrArray=new int[5] {5,9,10,99,0};//注意初始化列表必须和数组大小相对应
int[] myArray;
myArray=new int[5];//默认初始化为0

foreach循环是一种简便定位数组中的元素的方法,与for循环不同,foreach只能做只读循环。

foreach(string friendName in friendNames)
{
    WriteLine(friendName);
}

多维数组的定义和引用

double[,] hillHeight={{1,2,3,4},{2,3,4,5},{3,4,5,6}};
double maxHeight=hillHeight[2,3];
foreach(double Height in hillHeight)//循环顺序为先高位再低位([0,0]->[0,1]->...)
{
    if(Height>maxHeight)
        maxHeight=Height;
}

数组的数组(每维的元素个数可以不一样,即锯齿形数组)的声明(感觉没什么用呢?)

int[][] Distance={new int[] {1}, new int[]{1,2}};
foreach(int[] DdA in Distance)
{
    foreach(int Dd in DdA)
    {
        WriteLine(Dd);
    }
}

第六章 函数

一般采用PascalCase形式编写函数名。
函数中的所有处理路径最终都必须执行到return语句。
C#6引入一个表达式体方法(Lamda表达式):

static double Multiply(double myValue1, double myValue2)
{
    return myValue1*myValue2;
}
//以上函数现在可以用下面的Lamda表达式代替
static double Multiply(double myValue1, double myValue2) => myValue1*myValue2;

C#允许为函数指定一个(只能指定一个)特殊参数,这个参数必须是函数参数中的最后一个,称为参数数组。其允许使用个数不定的参数调用函数,可使用params关键字定义他们。

class Program
{
    static int SumVals(params int[]  vals)
    {
        int sum=0;
        foreach (int val in vals)
        {
            sum+=val;
        }
        return sum;
    }
    static void Main(string[] args)
    {
        int sum=SumVals(1,2,3,4,5);
        WriteLine($"Summed Values={sum}");
        ReadKey();
    }
}

使用关键字ref指定引用类型(注意,在函数定义和调用的时候都应使用该关键字)

static void ShowDouble(ref int val)
{
    val*=2;
}
int myVal=5;
ShowDouble(ref myVal);//Now myVal=10

注意,使用引用传递的变量必须经过初始化,且不能为常数类型(const)
可以使用out关键字输出参数,使用方法与ref类似,但是可以将未经初始化的变量指定out。在函数内的使用过程中也要将该变量当做未经初始化的变量。(使用out读着舒服)
尽量避免用全局变量传递参数(代码丑)

static int Main(string[] args)
//一般情况下,返回值为0表示程序运行正常。
//args为传给Main函数的参数,传入参数用空格隔开,储存在数组args里,当传入参数包含空格时用双引号包起来

可定义结构函数
函数重载中,返回值类型不是签名的一部分,因此不能用于重载。

6.6 委托

委托(delegate)是一种储存函数引用的类型。委托最重要的用途是事件和事件处理,这在后面介绍。委托的声明非常类似于函数,但不带函数体,且使用delegate关键字。委托的声明指定一个返回类型和一个参数列表。
定义了委托后就可以声明该委托的变量,接着把这个变量初始化为与委托具有相同返回类型和参数列表的函数引用,之后就可以用委托变量调用这个函数,就像该变量是一个函数一样。
有了引用函数的变量后,就可以完成一些之前无法完成的操作。例如,可以把委托变量当做一个参数传递给一个函数。

第七章 调试和错误处理

7.1 Visual Studio中的调试

VS允许在两种配置下生成应用程序(debug/release), debug模式下生成的程序包含符号信息,所以IDE知道执行每行代码时发生了什么,符号信息意味着跟踪未编译代码中使用的变量名,这样他们就可以匹配已编译的机器码应用程序中的现有值,而机器码程序不包括便于人们阅读的信息。此类信息包含在.pdb文件中,位于debug目录下。
release版本会优化程序,所以我们不能执行以上操作。但是release版本运行速度快(一般提供给客户release版本)。

7.1.1 非中断模式调试

1 输出调试信息

使用WriteLine()命令输出必要的信息到控制台可以观察程序的运行状态。在开发桌面应用程序等无控制台的程序时,可将文本输出到IDE的Output窗口。使用以下两个命令将信息输出到Output窗口:

System.Diagnostics.Debug.WriteLine()//仅在Debug模式下有效,不会编译到release版本
System.Diagnostics.Trace.WriteLine()

另外还可以创建一个日志文件,这与输出到Output窗口的情况类似。

2 跟踪点

另一个把信息输出到Output窗口的方法是使用跟踪点(Tracepoint),跟踪点是断点的一种。这是VS的一个功能,不是C#的功能,起作用与Debug.WriteLine()类似。跟踪点与断点类似,记录该行代码运行前程序的状态,其使用方法如下:
(1)在该行插入断点,右击断点选择setting;
(2)选中Actions复选框,在Message文本框输入要输出的字符串(变量名放在花括号中,正在运行的函数名可以用$FUNCTION表示);
(3)点击OK,断点符号变为菱形;
(4)可以在VS菜单中选择Debug|Windows|Breakpoints,显示添加的所有断点,在该窗口的columns下拉菜单中选择When Hit可以观察该断点的详细信息。
注意,跟踪点并没有包含在应用程序中,由VS处理,因此只有应用程序运行在VS调试器中才起作用。

7.1.2 中断模式调试

1 进入中断模式

断点可以配置为
(1)遇到断点进入中断
(2)遇到断点且条件判断为True,进入中断
(3)遇到断点一定次数,进入中断
(4)遇到断点且且自上次遇到断点时变量的值发生变化,进入中断
除在VS中配置中断外,还可在程序中添加中断(仅用于Debug版本):

Debug.Assert()
Trace.Assert()//可包含在realease版本中
//这两个函数带三个参数(中断条件(false时中断),输出到弹出对话框的字符串,输出到Output窗口的字符串)

2 监视变量内容

调试中在可以在Debug|Windows中定制窗口显示,其中Autos显示当前和前面语句中使用的变量,Locals显示作用域内的所有变量,Watch显示定制的变量。这些窗口不仅可以显示变量的值,还可以更改变量的值。

3 单步执行

VS进入中断后,马上要执行的代码旁会显示一个黄色箭头,可以拖动这个箭头来改变代码的运行轨迹。
单步执行分三个功能:(1)执行并移动到下一条指令;(2)同上,但不进入代码块;(3)执行到代码块的末尾处

4 Immediate和Command窗口

通过Command命令可以手动执行VS操作(如菜单,工具栏操作),Immediate窗口可执行与当前代码不同的额外代码,以及计算表达式(类似Python或Matlab的解释型程序),可用于变量修改(有点类似Watch窗口,但是功能更强大)。

5 Call Stack窗口

它描述了程序是如何执行到当前位置的(调用逻辑)。双击可移动到相应位置。

7.2 错误处理

有时候我们不能保证错误会不会发生,应编写足够简装的代码处理一些可能发生的问题,而不必中断程序的运行。

7.2.1 try…catch…finally

C#语言包含结构化异常处理语法。其基本结构如下:

try
{
    ...
}
catch( e) when(filterIsTrue)//( e)指异常类型,filterIsTrue指附加条件
{
    
    ...
}

finally
{
    
    ...
}

利用async和await关键字(C#6)的异步编程在本书中不讨论,但强烈建议学习他们
也可以只有try和finally而没有catch,或者有一个try和好几个catch,如果有catch, finally就是可选的,否则就是必须的。使用方法如下:
C#读书笔记_第1张图片

7.2.2 列出和配置异常

.NET Framework包含许多异常类型,IDE提供了一个exception对话框,可以检查和编辑可用的异常。使用Debug|Exceptions菜单选项可打开该对话框。每个异常都可以用右边的复选框编辑。使用(Break When)Thrown时,即使对已经处理(catch)的异常,也会进入中断。

第八章 面向对象编程介绍

8.1面向对象编程的含义

本章使用统一的建模语言(Unified Modeling Language,UML)语法研究类和对象。UML是为应用程序建模而设计的,从组成应用程序的对象,到它们执行的操作,到我们希望有的用例,应有尽有。UML是一个很专业的主题,很多图书专门讨论它,这里只使用这门语言的基本部分。
下图是一个打印机类的UML表示法。类名显示在这个方框的顶部。
C#读书笔记_第2张图片
下图是这个Printer类的一个实例myPrinter的UML表示方法。
C#读书笔记_第3张图片

1. 属性和字段

在类的UML表示法中,用第二部分显示属性和字段,如下图
C#读书笔记_第4张图片
这是CupOfCoffee类的表示方式,前面为它定义了5个成员(属性或字段,在UML中,它们没有区别),每个成员都包含下述信息:

  • 可访问性: +号代表是公有成员,-表示私有成员
  • 成员名
  • 成员的类型

2. 方法

在UML类框中,方法显示在第三部分,如下图:
C#读书笔记_第5张图片
其语法类似于字段和属性,最后显示的类型是返回类型,在这一部分,还显示了方法的参数。在UML中,每个参数都带有下述标识符之一:return,in,out或inout。他们用于表示数据的流向。其中out和inout大致对应于C#关键字out和ref。
实际上,C#和.Net Framework中的所有东西都是对象。控制台程序中的Main()函数就是类的一个方法。前面介绍的每一个变量类型上都是一个类。前面使用的每一个命令都是属性或方法。例如<String>.Length和<String>.ToUpper等。()区分了属性和方法。
每个对象都有一个明确的生命周期,除了“正在使用”的正常状态外,还包括构造阶段和析构阶段。

3. 构造函数

所有类定义都至少包括一个构造函数。C#中用new关键字来调用构造函数。构造函数与方法、属性、字段一样,可以是公有或私有的。可以通过把默认构造函数设置成私有的,就可以强制类的用户使用非默认的构造函数。
一些类没有公有的构造函数,外部代码不可能实例化他们,这些类称为不可创建的类,但是如稍后所述,这些类并不是完全没有用的。

4. 析构函数

一般情况下,不需要提供析构函数的代码,而由默认的析构函数自动执行操作。但是,如果在删除对象实例时需要完成一些重要的操作,就应提供具体的析构函数。例如,如果变量超过了范围,代码就不能访问它,但该变量仍存在于计算机内存的某个地方。只有在.Net运行程序执行其垃圾回收,进行清理时,该实例才被彻底删除。
静态属性和静态字段可以访问独立于任何对象实例的数据,静态方法可以执行与对象类型相关但与对象实例无关的命令。只是用静态成员时,甚至不需要实例化对象。
例如前面使用的Consol.WriteLine()和Convert.Tostring()方法就是静态的。许多情况下,静态属性和静态方法有很好的效果。例如,可以用静态属性跟踪类创建了多少个实例。在UML语法中,类的静态成员带有下划线。

5. 静态构造函数

使用类中的静态成员时,需要初始化这些成员。在声明时,可以给静态成员提供一个初始值,但有时需要执行更复杂的初始化操作,或者在赋值、执行静态方法前执行某些操作。
使用静态构造函数可以执行此类任务。静态构造函数不能直接调用,只能在下述情况下执行:
- 创建包含静态成员的实例时
- 访问包含静态构造函数的类的静态成员时
在这两种情况下,会首先调用静态构造函数,之后实例化类或访问静态成员。无论创造了多少个类实例,其静态构造函数都只掉一次。为了区分,也将非静态构造函数称为实例构造函数。

6. 静态类

我们常常希望类只包含静态成员,且不能用来实例化对象(如Consol)。一种简单的技术是使用静态类,而不是把类的构造函数设为私有。静态类只能包含静态成员,不能包含实例构造函数。

8.2 OOP技术

8.2.1 接口

接口就是把公共实例(非静态)方法和属性组合起来,以封装特定功能的一个集合。一旦定义了一个接口,就可以在类中实现它 。这样,类就可以支持接口所指定的所有属性和成员。
注意,接口不能单独存在。不能像实例化一个类一样实例化一个接口。另外,接口不能包含实现其成员的任何代码,只能定义成员本身。实现过程必须在实现接口的类中完成。接口名一般以大写字母I开头。
在UML中,在对象上实现的接口用棒棒糖语法来表示。用与类相似的语法把成员放在一个单独的框中。
C#读书笔记_第6张图片
一个类可以支持多个接口,多个类也可以支持相同的接口。接口的概念让用户和其他开发人员更容易理解其他人的代码。例如,有一些代码使用一个带某个接口的对象。假定不使用这个对象的其他属性或方法,就可以用另一个对象代替这个对象。(上图中的IHotDrink接口代码可以处理CupOfCoffee实例和CupOfTea实例)。另外,该对象的开发人员可以提供该对象的更新版本,只要它支持已经在用的接口,就可以在代码中使用这个新版本。
发布接口后,最好不要修改它,应当做的是创建一个新接口,使其扩展旧接口,可能还包含一个版本号,如X2。这是创建接口的标准方式,以后会常遇到已编号的接口。
IDisposable接口用于删除对象,支持IDisposable接口的对象必须实现Disposable()方法。当不需要某个对象(如对象超出作用域时),调用这个方法释放重要的资源,否则,等到垃圾回收时调用析构方法时才会释放该资源。这样可以更好的控制对象的资源。
使用using关键字可以方便的实现这一机制,方法如下:

  = new ();
...
using ()
{
    ...
}
或者:
using ( =new ())
{
    ...
}

这两种情况中,可在using代码块中使用变量,并在代码块的结尾自动删除(调用Disposable())。

8.2.2 继承

在OOP中,被继承(派生)的类被称为父类(基类)。C#中的对象仅能派生于一个基类。在UML中,用箭头表示继承,如下图所示:
C#读书笔记_第7张图片
为简洁起见,上图中省略了返回值类型。
派生类不能访问基类的私有成员,但可以访问其公有成员和保护成员。也就是说外部代码只可以访问公有成员,派生类可以访问公有和保护成员,类自己可以访问所有成员。
基类的成员可以是虚拟的,也就是说成员可由继承它的类重写。这种实现不会删除原有代码,仍可以在类中访问原来的代码,但外部代码不能访问它们。注意虚拟成员不能是私有成员,不能既要求派生类重写,又不让派生类访问。
基类还可以定义为抽象类,不能直接实例化。要使用抽象类,必须继承这个类。抽象类可以有抽象成员,这些成员在基类中没有实现代码,派生类必须实现它们。如果Animal类为抽象类,则上图可表示为:
C#读书笔记_第8张图片
图中EatFood()和Breed()都显示在派生类中,说明这两个方法是抽象的(必须在派生类中重写)或虚拟的(这里已经在派生类中重写)。
最后,类可以是密封(seal)的。密封类不能作为基类,没有派生类。
在C#中所有对象都有一个共同的基类object(在.Net Framework中,它是System.Object的别名)。
注意,接口也可以继承自其它接口,与类不同的是,接口可以继承自多个接口(与类可以支持多个接口的方式类似)。

8.2.3 多态性

可以把某个派生类型的变量赋给基本类型的变量,如:

Cow myCow = new Cow();
Animal myAnimal = myCow;

不需要强制类型转换就可以通过这个变量调用基类的方法:

myAnimal.EatFood();//调用派生类中的EatFood()的实现代码

不能用相同的方法调用派生类中的方法:

myAnimal.Moo();//这是错的

但可以把基本类型的变量转换为派生类型的变量,然后调用派生类方法,如:

Cow myNewCow = (Cow)myAnimal;
myNewCow.Moo();

如果原始变量的类型不是Cow或派生于Cow的类型,这个强制转换就会引发一个异常。有许多方式说明对象的类型是什么,详见下一章。
并不只是共享一个父类的类才能利用多态性,只要子类和孙子类在继承结构中有一个相同的类,就可以用同样的方式利用多态性。
接口也有多态性,虽然接口没有实例,但是可以建立接口类型的变量。例如,假定不使用基类Animal提供的EatFood()方法,而是把该方法放在IConsume接口上。Cow和Chicken类也支持这个接口,唯一的区别是他们必须提供EatFood()方法的实现代码(因为接口不包含实现代码):

Cow myCow = new Cow();
Chichen myChicken = new Chicken();
Iconsume consumeInterfce;
consumeInterfce = myCow;
consumeInterfce.EatFood();
consumeInterfce = myChicken ;
consumeInterfce.EatFood();

这就提供了用相同方式访问多个对象的简单方式,且不依赖于公共的基类。
注意,派生类会继承其基类支持的接口,当然,有共同基类的类不一定有共同接口(基类无接口),反之亦然(有共同接口的类不一定有共同基类)。

8.2.4 对象之间的关系

继承是对象之间一种简单的短息,对象之间还有其他的一些重要关系。这里简单讨论下述关系:

  • 包含关系
    一个类包含另一个类,类似于继承,但是包含类可以控制对被包含类的访问,甚至在使用被包含类成员前先进行其他处理。
    用一个成员字段包含对象实例(在类内声明一个其他类的变量),就可以实现包含关系。在UML中,被包含类可以用关联线条来表示。对于简单包含关系,可以用带有1的线条说明一对一的关系。
    C#读书笔记_第9张图片
  • 集合关系
    集合基本上就是增加了功能的数组,集合以与其他对象相同的方式实现为类。它们通常以所存储的对象名称的复数形式来命名。集合与数组不同,其通常实现额外功能,例如Add()和Remove()方法,而且集合通常有一个Item属性,它根据对象的索引返回该对象,通常,这个属性还允许实现更复杂的访问方式,例如可以设计一个Animals,让Animal对象根据其名称来访问。
    其UML表示如下图,图中,连线末尾的数字表示一个Animals对象可以包含0个或多个Animal对象。第11章详述集合。
    C#读书笔记_第10张图片

8.2.5 运算符重载

在类内实现运算符重载,详见13章。

8.2.6 事件

对象可以激活和使用事件,作为它们处理的一部分(类似于异常,但功能更强大)。本章后面介绍Windows应用程序中时间的工作原理,第13章深入讨论事件。

8.2.7 引用类型和值类型

变量的类型分为两种,其区别如下:

  • 值类型在内存的同一处储存它们自己和它们的内容。
  • 引用类型储存指向内存中其他某个位置(堆)的引用。

一个主要区别是:值类型总包含一个值,但引用类型可以是null,表示它们不包含值。我们穿件的每个类、数组都是引用类型。

第九章 定义类

9.1 C#中的类定义

C#使用class关键字定义类。

class MyClass
{
}

默认情况下,类声明为内部的,即只有当前项目中可以访问,可以用internal关键字显式的知名这一点(但没有必要):

internal class MyClass
{
}

也可用public关键字指明类是公共的(可由其他项目的代码访问)。还可指定类是抽象的(不能实例化,只能继承,可以有抽象成员)或密封的(不能继承),关键字为abstract和sealed。
还可以在类定义中指定继承,冒号后是基类名:

public class MyClass : MyBase
{
}

在C#中,只能有一个基类,如果继承了一个抽象类,就必须实现基类的所有成员(除非派生类也是抽象的)。编译器不允许派生类的访问性高于基类,因此内部类可以继承自一个公共基类,但公共类不能继承自一个内部类,因此下述代码是非法的:

public class MyClass : MyBase
{
}
internal class MyBase
{
}

可以用相似的方式指定类的接口:

public class MyClass : IMyInterface
{
}

当同时指定基类和接口是,基类名在前,接口名在后,用逗号隔开

public class MyClass : MyBase, IMyInterface1, IMyInterface2
{
}

支持接口的类必须实现所有接口成员,如果不想使用给定的接口成员,也必须提供一种“空”的实现方式(没有函数代码)。还可以将接口成员实现为抽象类中的抽象成员。
接口的定义方式与类相似,但使用interface关键字(而不是class关键字),包含public,internal关键字(默认是internal的),但没有abstract和sealed关键字(接口不能被实例化,且必须是可以继承的)。可以用与类相似的方式指定接口的继承。

9.2 System.Object

所有的类都继承自System.Object,因此这些类都可以访问该类中受保护的成员和公共成员。下表列出了其包含的方法:

方法 返回类型 虚拟 静态 说明
Object() 构造函数,自动调用
~Object()也叫Finalize(),参见下节 析构函数,自动调用,不能手动调用
Equals(object) bool 把调用该方法的对象与另一个对象相比,默认实现会检查是否引用了同一个对象(因为对象是引用类型),但该方法可以重写(使用时一般都会重写。否则就别用)
Equals(object,object) bool 检查时调用Equals(object),注意,如果比较双方都为空引用,返回true
ReferenceEquals(object,object) bool 比较是否为同一个实例的引用(安全的比较)
ToString() String 返回类的名称(可以重写)
MemberwiseClone() object 用于浅克隆(引用克隆)
GetType() System.Type 以System.Type对象的形式返回对象类型
GetHashCode() int 用作对象的散列函数,返回一个以压缩形式表示对象状态的值???

在利用多态性时,GetType()是一个有用的方法,结合typeof(可以把类名转换为System.Type对象)可以根据对象的不同执行不同操作。

if(myObj.GetType()==typeof(MyComplexClass))
{
}

未来章节会反复讨论System.Object方法。

9.3 构造函数和析构函数

采用private的默认构造函数,可以使类无法用默认方式创建实例(强制使用非默认的构造函数)。
类的析构函数由带有~前缀的类名声明。调用析构函数后会隐式的调用基类的析构函数。
发生构造函数调用错误常常是因为类继承结构中的某个基类没有正确实例化,或者没有正确的给基类构造函数提供信息。
如果要实例化一个派生类,必须实例化它的基类,因此,无论使用什么构造函数实例化一个类,总是首先调用System.Object.Object()。除非明确指定,无论在派生类上使用什么构造函数,将使用基类的默认构造函数构造。
采用如下方法可以指定基类的构造函数:

public class MyClass : MyBaseClass
{
    public MyClass(int i, int j):base(i)
    {
    }
    public MyClass():base(10)
    {
    }
}

还可以使用关键字this,指定使用其他构造函数,如下例:

public class MyBaseClass
{
    public MyBaseClass()
    {
    }
    public MyBaseClass(int i)
    {
    }
}
public class MyClass : MyBaseClass
{
    public MyClass():this(5,6)
    {
    }
    public MyClass(int i, int j):base(10)
    {
    }
}

上例中使用MyClase.MyClass()构造函数的调用顺序如下:

  • System.Object.Object()
  • MyBaseClass.MyBaseClass(int i)
  • MyClass.MyClass(int i,int j)
  • MyClass.MyClass()

注意,如果没有指定构造函数初始化器,编译器会自动添加base()为构造函数初始化器(对复杂的构造函数推荐显式的指定初始化器,使结构层次清晰)。同时注意不要在定义构造函数时造成无限循环。

9.4 Visual Studio 中的OPP工具

9.4.1 Class View窗口

在Class View 窗口中可以查看我们使用的类的特性。注意不同图标代表的含义,包括项目、属性、事件、名称空间、字段、委托、类、结构、程序集、接口、方法、枚举、枚举项,还包括它们的访问级别:私有的、受保护的、内部的。没有符号用于表示抽象、密封和虚拟项。
可以双击访问相关代码,或右击Browse Definition查看详情。也可以在Project References中查看项目引用的程序集。在此窗口中还可以进行重命名。

9.4.2 Object Browser窗口

对象浏览器中可以查看项目中能使用的其他类,甚至查看外部的类。
右下角的信息窗口中显示了方法签名、所属的类和方法功能小结,用户可以自己定义和使用这个信息窗口,在类的定义前增加如下修改可以将信息显示在该窗口:

/// 
/// This class constains my program
///

这是XML文档说明额一个示例,有闲暇时应学习这个主题。

9.4.3 添加类

Vs包含一些加速执行某些常见任务的工具。单价Project|Add New Item,或在Solution Explorer窗口中右击项目,选择相应的项,可以打开该工具。为文件提供一个文件名,所创建的类/接口…名称即为文件名称。

9.4.4 类图

在Solution Explorer窗口中右击项目,选择View|View Class Diagram菜单,可以生成类似UML的图,自Toolbox中,可以给图添加新项,如类、接口、枚举等,此时代码会自动生成。

9.5 类库项目

如果一个项目中仅包含类(以及其他相关的额类型定义,但没有入口),就成该项目为类库。
类库项目编译为.dll程序集,在其他项目中添加对类库的应用就可以访问他的内容。这扩展了对象提供的封装性,因为修改和更新类库不会影响使用它们的其他项目。
注意默认情况下,会将类隐式的确定为供内部访问(internal),最好明确的指定可访问性(External Or Internal),以便于理解。
当应用程序使用外部库中定义的类时,可以把应用程序称为该库的客户程序,使用所定义的类的代码称为客户代码。
要使用类库中的代码,应添加对Dll的引用(add|reference)。开发时,建议将Dll复制到本地位置,库更新时用新Dll代替旧Dll即可。
添加引用后,就可以使用对象浏览器查看可用的类(看不到内部类)。

9.6 接口和抽象类

这两种类型在许多方面都十分类似,首先讨论他们的相似之处。
抽象类和接口都包含可以由派生类继承的成员,抽象类和接口都不能直接实例化。
二者的区别:派生类只能继承自一个基类,相反,类可以使用多个接口。抽象类可以有抽象成员(没有代码体,且必须在派生类中实现)和非抽象成员(有代码体,但可以是虚拟的,这样就可以在派生类中重写)。另一方面,接口成员必须都在使用接口的类上实现——它们没有代码体。另外,接口成员是公共的(它们的目的是在外部使用),而抽象类的成员可以是私有的、受保护的。此外,接口不能包含字段、构造函数、析构函数、静态成员或常量。

9.7 结构类型

结构和类非常相似,但结构是值类型,类是引用类型。类在赋值时,赋的是地址,不会进行深度Copy,而结构赋值时则直接进行深度Copy。

9.8 浅度和深度复制

从一个变量到两一个变量按值复制对象,而不是按引用复制对象可能会十分复杂(因为变量的成员本身有可能也是引用)。简单的按照成员复制对象可以通过派生于System.object的MemberwiseClone()方法完成,这是一个protected方法(只能被派生类和基类访问,不能被外部成员访问),很容易在对象上定义一个调用该方法的公共方法(在派生类中添加一个调用该方法的公共方法)。这种赋值为浅度复制(shallow copy)。
可以实现一个ICloneable接口,以标准方式进行深度复制(很多人建议不要使用它),在11章详细介绍这个方法。

第十章 定义类成员

10.1 成员定义

10.1.1 定义字段

使用统一的格式为变量起名可以提高代码的可读性(Public字段用PascalCasing形式命名,private字段用camelCasing形式命名),字段可以用readonly关键字修饰,表示这个字段只能在构造函数中赋值(或由初始化赋值语句赋值)。可以用static修饰静态变量(静态字段必须通过定义他们的类来访问),也可以用const创建常量值,常量是自动静态的。

10.1.2 定义方法

可以用static(静态的)、virtual(虚拟的,可以重写)、abstract(抽象的,必须重写)、override(如果方法被重写就必须使用该关键字)、extern(方法定义在其他地方(这是一个高级论题,这里不讨论))、sealed(不能被重写)修饰方法。

10.1.3 定义属性

属性定义与字段类似,但是包含的内容更多,属性有两个类似函数的块——访问器,set和get。可以通过忽略其中一个块来创建只读或只写属性。当然这只适用于外部代码,因为类中的其他代码可以访问这些数据。可以在访问器上包含可访问修饰符(Public、Private),但访问器的可访问性不能高于他所属的属性
get块必须有一个属性类型的返回值。用关键字value表示用户提供的属性值。value的类型为属性类型。
像方法一样,属性可以使用virtual、abstract、override关键字,可以用Lambda表达式定义属性。

10.1.4 添加属性

添加属性时有一种方便的方法,从字段中生成属性。右击某个成员,选择Quick Actions…。

10.1.5 自动属性

用下例中的方法,编译器会自动补全代码,效果与标准的属性相同

public int MyIntProp
{
    get;
    set;
}

VS中可以通过键入prop,然后按两次Tab键完成自动创建。

10.2类成员的其他主题

10.2.1 隐藏基类的方法

默认情况下的重写会隐藏基类的方法,可以用new关键字显示的表明意图:

Public class MyClass:MyBaseClass
{
    new public void DoSomeThing()
    {
    }
}

但是要注意隐藏与虚拟函数重写的区别,尤其是使用多态时

public class BaseClass
{
    public virtual void DoSomeThing()=>WriteLine("Base");
}
public class MyClass1:BaseClass
{
    public override void DoSomeThing()=>WriteLine("not Base");
}
public class MyClass2:BaseClass
{
    new public void DoSomeThing()=>WriteLine("not Base");
}
MyClass1 MyObj1=new MyClass1();
MyClass1 MyObj2=new MyClass2();
BaseClass BaseObj1=MyObj1;
BaseObj1.DoSomeThing();//not Base
BaseClass BaseObj2=MyObj2;
BaseObj2.DoSomeThing();//Base

10.2.2 调用重写或隐藏的基类方法

使用base关键字进行调用

public class BaseClass
{
    public virtual void DoSomeThing()
    {
    }
}
public class MyClass1:BaseClass
{
    public override void DoSomeThing()
    {
        base.DoSomeThing();
    }
}

由于base使用的是对象实例,而静态成员不是实例的一部分,因此不能在静态成员中使用它。类似的,this可以引用当前的实例对象,最常用的功能是把当前对象的引用传递给一个方法。或者限定范围(指定变量为类内变量而非其他变量)。

public void DoSomeThing()
{
    MyTargetClass myObj=new MyTargetClass();
    myObj.Show(this.someData);
}

10.2.3 嵌套的类型定义

在类中(而非名称空间中)定义类,即为嵌套定义,嵌套定义可以用new隐藏基类方法。嵌套定义的类实例化时,必须限定名称(当然,当嵌套定义的类为私有时,就不可以在外部实例化)。这个功能用于定义对于包含类来说是私有的类,这样外部就不能访问它,除此之外,这种定义方法还可以访问其包含类的私有和受保护成员(区别于继承)。
这种使用方法很灵活,要小心使逻辑清楚。

10.3 接口实现

接口成员的定义与类成员的定义类似,但有几个区别:不能使用访问修饰符(所有成员都是隐式公共的)、不包含代码体、不能定义字段、不能用静态、虚拟、密封、抽象修饰、不能包含类型定义(不能在接口中定义接口或类)。
可以用new隐藏基接口的成员,接口定义中的属性可以指定访问块中使用set和get中的哪个。
接口不能指定字段,因此不能指定如何储存属性数据。接口可以被定义成类的成员。
实现接口的类必须包含接口所有成员的实现代码,且必须匹配签名(包括匹配指定的set和get块),且必须是公共的。
可以使用virtual和abstract实现接口成员,但不能用static和const。还可以在基类上实现接口成员。继承一个实现指定接口的类,派生类也会隐式的支持这个接口。在基类中把实现代码定义为虚拟,派生类就可以重写该实现,但是如果在派生类中隐藏该基类成员,则接口就只能访问基类中的实现(即使是通过派生类访问也是这样)。
如果接口成员是显示实现的,那么就只能通过接口访问该成员,如果接口成员是隐式实现的,那么既能用接口访问,也能用类访问该成员

public class MyClass:IMyInterface
{
    void IMyInerface.DoSomeThing(){}//只能通过接口访问
    void DoSomeThingElse(){}//可以用两种方式访问
}

前面提到,实现接口的类必须包含接口所有成员的实现代码,且必须匹配签名(包括匹配指定的set和get块),且必须是公共的。实际上接口可能只包含set或get,隐式实现接口时,可以添加非公共的存取器。如果新添加的存取器是公共的(不匹配),那么就只能通过类访问该存取器,不能通过接口访问该存取器(接口中没有定义)。

10.4 部分类定义

在代码的定义中可以折叠和展开区域,以便于阅读。

#region Fields
...
#endreigion
#region Methods
...
#endreigion

另一种方法是使用部分类定义,把类的定义放在多个文件中。使用partial关键字进行部分定义

public partial class MyClass
{
}

注意,对于继承,在部分定义的任意部分指定都是有效的,但是由于C#中的基类只能有一个,所以如果在多个位置指定了基类,那么它们必须是同一个基类(推荐在类的每个部分定义中都指定类和接口的继承,阅读上比较方便)。

10.5 部分方法定义

在一个部分类中定义,在另一个部分类中实现,两个部分类中都使用partial关键字。这称为部分方法。部分方法的使用有诸多限制,如不能有返回值,不能有out参数,不能被virtual、abstract、override、new、sealed、extern修饰。主要作用是在定制自动生成的代码或设计器创建的代码时使用(用户可以选择是否实现,如不实现,也要保证程序运行正确)。

10.6 例子

这里介绍了一个纸牌类的例子,用到了代码自动生成,可以多看看。

第十一章 集合、比较和转换

11.1 集合

集合如字面意思所示,数组(System.Array)、高级数组(System.Collecton.ArrayList)都是集合类(Collection Class)的一种。System.Collection名称空间中的几个接口提供了基本的集合功能。

11.1.1 使用集合

相比Array,ArrayList有更加强大的功能,包括:
1.定义时可以不指定数组的大小
2.有更加灵活的构造函数,支持用复制的方法进行初始化
3.如果创建时指定了大小,而使用中集合中的项数超过了这个数值,容量会自动增加一倍
4.使用Add()方法添加新项
5.同样支持下标引用(实现了IList接口)
6.同样支持foreach结构(实现了IEnumberable接口)
7.同样支持对长度的查询(实现了ICollection接口,使用该接口的Count属性进行访问)
8.不同于Array,必须对所有的项进行数据类型转换
9.可以删除项(IList接口的一部分)
10.可以一次添加多各项(使用AddRange()方法访问,这个方法接受带有ICollection接口的任意对象(如Array或ArrayList)),AddRange()方法专用于ArrayList类,不是任何接口的一部分,证明了可以在集合类中定制操作,这个类还支持其他的定制操作(如排序)。
注意,由于数组时引用类型,用一个长度初始化数组,并不能使用其中的项(需要先对该项进行初始化,即需要给这个项赋予初始化了的对象),这也意味着,抽象类可以拥有数组(数组中的项可以初始化为其子类对象。用多态方法进行调用)。

11.1.2 定义集合

实现自己的强类型化的集合,一种方法是手动实现需要的方法(费时且复杂),还可以从一个类中派生自己的集合,例如System.Collection.CollectionBase类,这个抽象类提供了集合类的大量实现代码,这是推荐使用的方式。
主要使用的CollectionBase类的实现方法为Clear()、RemoveAt()以及ICollection的Count属性,其他实现代码需要自己提供。
可以使用CollectionBase的两个受保护属性List和InnerList访问储存的对象,这里提供一个例子:

public class Animals : CollectionBase
{
    public Add(Animal newAnimal)
    {
        List.Add(newAnimal);
    }
}

11.1.3 索引符

索引符是一种特殊类型的属性

public class Animals : CollectionBase
{
    ...
    public Animal this[int animalIndex]
    {
        get { return (Animal)List[animalIndex]; }
        set { List[animalIndex] = value; }
    }
}

注意这里的强制转换,IList的List属性返回一个System.Object对象,现在返回的则是Animal类型的对象。
有了这个实现,11.1.1中的第8条就可以很清楚了,在这里实现的下标索引在使用时就不必进行强制类型转换了。

你可能感兴趣的:(C#笔记)