金旭亮C#编程课个人自学笔记

/ 0评 / 2赞 / 

前言

.NET是什么

.NET是一个由微软公司研发的软件运行平台,现在基于Windows平台开发应用程序,大多采用.NET作为核心编程模型。

基于.NET平台开发软件,其中一个特点就是支持混合语言开发,微软官方维护的三种.NET编程语言为:C#、VB、F#。因为无论使用哪种语言开发,都要编译为同一种中间语言。

可以简单地把这些中间语言理解为.NET虚拟计算机所能直接执行的汇编语言。

只要编译器能把某种语言编译为此中间语言,那么他就能在.NET平台上运行。

新的.NET Core可以跑在各种平台上,不只局限于Windows。

新一代的编程模型

Windows的.NET Framework平台构成为 CLR(Common Language Runtime) + 基类库(包容大量可重用软件组件)

Visual Studio编写C#基础知识

计算机运行程序流程

使用0和1表达信息

数值信息:直接使用二进制表达

非数值信息:采用"编码"。比如五笔输入法用一到四个字符对应一到多个汉字,而图像用数字表达每一个像素点和位置等信息,以特定格式写入到文件。

"编码"就是以若干位数码(或符号)的不同组合来表示非数值类型的信息,他人为地为数码(或符号)的每一种组合指定了一种特定的含义

两种不同的内存类型

物理内存:单台计算机上安装的物理储存芯片所提供的内存

虚拟内存:由操作系统在硬盘上划出一块空间所提供的虚拟内存,可以比物理内存大很多。

使用C#所写的.NET程序,无法直接访问物理内存上的特定存储单元,它所访问的是由操作系统(如Windows)所提供的虚拟内存

HelloWorld

使用记事本时:保存文件为.cs文件,编码ANSI,然后对其进行编译:

搜索command,可以看到VS所提供的命令提示符 如x64 Native Tools Command Prompt for VS 2019,打开后转到.cs文件保存的目录

控制台程序输入输出

C#的输出也可以使用占位符的方式,感觉形式上类似于c语言的%s

占位符的好处在于方便的拼合多个字符串

例:

才发现之前的两篇笔记没有记转义字符表

转义字符意义ASCLL码值(十进制)
\a响铃(BEL)007
\b退格(BS) ,将当前位置移到前一列008
\f换页(FF),将当前位置移到下页开头012
\n换行(LF) ,将当前位置移到下一行开头010
\r回车(CR) ,将当前位置移到本行开头013
\t水平制表(HT) (跳到下一个TAB位置)009
\v垂直制表(VT)011
\'代表一个单引号039
\"代表一个双引号字符034
\代表一个反斜线字符'''092
?代表一个问号063
\0空字符(NUL)000
\ddd1到3位八进制数所代表的任意字符三位八进制
\xhh十六进制所代表的任意字符十六进制

ReadKey和Beep

比如两次readkey都输入一个A控制台内则会如下显示:

计算机beeeee一下

C#基本编程规则

标识符(identtifier,即程序中拥有特殊含义的单词,比如类或者方法):区分大小写

每条语句以分号结尾,多条语句可以使用{}组合为一个语句块

编写的代码放在类(class)中,类是C#编程的基本单元

存放C#类的源代码文件其扩展名为".cs",一个".cs"文件中可以存放多个C#类

Visual Studio中的文件组织方式

一个解决方案可以含有多个项目,项目(Project)是VS编译的最小单元

设置为启动项目的项目在启动的时候会启动

面向对象程序设计概述

windows平台下,可以执行的文件通常是以.exe结尾的。另有一种扩展名为.dll的文件(称为“动态链接库”)它需要被.exe装入后才能执行

.NET运行机理

编译是怎么回事

纯文本形式的源代码—提交—C#编译器编译源代码—生成—二进制形式的可执行文件(HelloWorld.exe)

"编译(compile)"有点类似于"翻译",粗略的说,它负责把人编写的源代码"翻译"为计算机可以识别并执行的二进制代码。

而可执行程序也是分类的:

可执行程序文件也分多种类型,在Windows平台下可分为:

本课程学习的基本上都是托管程序

“托管的”应用程序是什么意思:

.NET应用程序是“托管(Managed)”的,意思是他必须在一个独立的运行环境(即CLR)中运行,并受到这个运行环境的管理与控制。没有环境程序无法运行

CLR:Common Language Runtime,通用语言运行时。是.NET Framework应用程序的运行环境,可以看成是一台专用于运行.NET应用程序的虚拟机(Virtual Machine)。

到了.NET Core时代,.NET虚拟机名字前也加了一个Core,称为“CoreCLR”

.exe和.dll在.NET中统称为 “程序集(Assembly)”,在程序集这种文件中保存的是二进制的指令,在.NET中被称为中间语言指令。

怎样构造求解问题的算法

计算机中的算法,主要指为了解决某个问题而设计的一种解决方案,包容一系列计算机能够执行的有着特定顺序的命令,计算机执行完这些命令,将得到某种结果,意味着某个问题己经有了一个结论。

算法的针对性很强,专用于解决特定的问题。

算法的设计,通常与数学有着很密切的联系,并且是独立于特定的编程语言和软件平台的。这就是说:

可以使用多种编程语言,以多种方式,在不同的平台上实现同一个算法。

从“结构化”到“面向对象”

结构化方法是一种历史悠久的软件开发方法,是面向对象方法的前身。

程序设计可以看成是一种"抽象"的艺术

使用“抽象”的思维方式,构造软件系统的顶层模型

数据结构——对数据进行抽象

先确定一种数据结构 ,然后基于此数据结构设计算法

MyDate这个数据结构,封装了 “年”“月”“日”三个基本信息

在程序设计中,依据要解决的特定问题,分析它所涉及的相关数据和其中所蕴含的各种信息,按照特定编程语言所支持的语法特性,将它们转换为特定的数据结构,往往是整个开发中至关重要的一步。

基于数据结构确定算法

将人的计算方法转为计算机算法:

(1) 计算出两个日期之间的整年天数
(2) 计算出两个日期之间的整月天数(去掉中间的整年)

每个算法步骤用(定义)一个函数来实现:

CalculateDaysBetweenTwoYear()
CalculateDaysBetweenTwoMonth()

算法就是一系列的命令,计算机通过执行这些命令,完成特定的数据处理工作。

进一步细化与调整设计方案

需要判断是否是闰年,所以应该添加一个IsLeapYear()函数。

再添加一个顶层主控函数CalculateDaysBetweenTwoDate(),将前面设计得到的函数“装配”起来,从而实现整个算法。

最终的技术设计方案

如何确定开发顺序?

确定开发顺序的基本方法

具体开发时,“从下层到上层”地逐层开发,就象盖楼一样....

①先开发那些被别人调用,自己不调用别人的函数 IsLeapYear()

②↓开发中间层函数,它需要调用底层已经实现好的函数

CalculateDaysBetweenTwoYear()
CalculateDaysBetweenTwoMonth()

③↓↓开发顶层函数,它需要调用中间层已经实现好的函数,通常情况下,避免跨层调用。

CalculateDaysBetweenTwoDate()

在结构化的编程中,有一个基本的编程原则就是尽量的避免出现跨层调用的情况,比如主控函数不要调用判断闰年的函数,而反过来则更不允许,底层的函数绝对不能调用高层的函数。

转为面向对象实现

将函数们移动到一个新的类里——

重大变化:

  1. 新的类DateCalculator的指责很明确,它负责“计算日期”,除此之外,它什么也不干。
  2. 外界只能“看到”并调用它所定义的唯一一个“公有(public)”方法CalculateDaysOfTwoDate(),根本就不知道它内部到底是怎么计算出来的。

也就是将除了主函数之外的函数设为私有,仅公开主函数。

面向对象带来的好处:

  1. 从使用者角度,DateCalculator类因为 “简单”,所以“易用”。
  2. 具体计算日期的算法被封装到了DateCalculator类的内部,在必要时可以修改算法,外部调用者不会受到影响,其调用代码不需要改变。

一个"偷懒"的方法:

.NET基类库中内置了日期处理的相关功能,可以直接使用它来完成计算两个日期之间相隔天数的问题。

从这个示例中,我们可以看到,如果有一个功能强大的组件库,基于这些组件开发应用程序,可以大大地提升软件开发效率,因为我们可以重用别人的工作,不再需要一切从头开始。

C#结构化编程基础

理解变量

程序运行所需要处理的数据,通常都需要放到“变量(variable)”中。

变量可以看作是一种数据容器。

不同类型的“容器”,适合放置不同类型的数据,这种“类型”,我们称它为“变量的数据类型”。

上述语句定义了一变量,它的类型是“int(整型)”,可以放置一个整数,它最初“装入”的数是“100”。

C#语句以“分号”结束,因此分号不能省,省略了之后,代码将无法编译。

变量“生活”在“内存(memory )”中

应用程序中能使用的内存,是由操作系统提供的“虚拟内存”,其物理载体通常是我们在前一单元视频中介绍过的内存条和硬盘。

内存由多个内存单元构成,每个内存单元都有一个编号,称为“内存地址”。

给定一个“内存地址”,就能找到特定的内存单元。

计算机可以从内存单元中写入或读出数据。

使用比较底层的编程语言,比如“汇编语言(Assembly Language)”,我们可以直接指定地址去存取特定的内存单元,但这么干“太低效了”,为什么?

  1. 不同的计算机硬件,可以访问的物理内存数量是变化的 比如搭载的内存条不同
  2. 不同的操作系统,甚至是同一操作系统的不同版本,对内存单元的存取可能都有着不同的要求
    例如32位的Windows能够访问的最大内存是4G.

你必须针对特定的计算机去写“特定”的程序,硬件略有变化,如果不作修改,你的程序可能就崩溃了……

因此这种编程方法是行不通而且十分低效的。

计算机科学家给出的解决方案是……

  1. 应用程序需要存取数据时,不指定具体的内存地址值,而只是给出一个“名字”,这个“名字”引用某块内存区域。
  2. 这个“名字”具体到底引用的是哪块内存区域,由操作系统(Windows)负责安排,应用程序就不要费这个心了!
  3. 应用程序根本就不管它要用的数据具体保存在哪个地方,它只知道,我可以“按名”存取数据就够了!

这个内存区域的“名字”,就是我们所讲的“变量”!

通过变量名“间接”地存取内存

int i = 0;

变量名—对应于—某块内存区域的“别名”

  • 变量的名字(上面代码中的“i ”),可以看成是特定内存区域的 “别名”,通过它计算机就能找到特定的内存单元存取数据。
  • 有了变量名,就不再需要显式指定一长串的地址数值来访问内存单元了。

“给变量赋值”是什么意思?

变量定义好之后,可以随时地通过赋值语句传给它不同的值……

C#代码中的单个等号,称为“赋值符”,与数学中的等号是两回事,它表示要把等号右边的值,传给左边的变量。

给变量赋值,其实就是找到变量所代表的内存区域,把指定的数值写入其中。

注意:写入到内存单元中的数据,都被转换为二进制数值,计算机并不能直接处理我们常用的十进制数。

变量间的相互赋值,本质上是内存单元间的值复制
在上述代码中,j = i,其实就是把“i”中所保存的数值复制一份,然后保存到“j”所代表的内存单元中。
注意:变量“i ”和“ j”之间是完全独立的,修改“i”的值,不会导致“j”的值同步变化。

数据类型

四种最常用的数据类型

如果希望知道一个变量的数据类型:

调用GetType()方法

输出如下:

另外,也可以使用另一种方法tyoepf();用来验证某一个变量是否是特定的类型

我们因此得知,我们可以用GetType获取变量的类型,用typeof验证变量的类型。

因为intValue是int类型,所以上述WriteLine会输出一个bool:true

由上可以看出C#内置的数据类型是对应着CLR支持的基础数据模型的

intSystem.Int32
longSystem.Int64
floatSystem.Double
doubleSystem.Single

我们使用的这些C#的内置数据类型在编译的时候会被转变为CLR支持的基础的数据类型

C# 7中对整数的改进

C# 7之后的版本,对于比较大的整数,可以使用下划线对其进行分割,以方便人阅读:

对于二进制数值(以“0b”打头),也可以直接地添加下划线进行分割:

String和var

在定义字符串的时候,首字母大写与小写都可以成功定义:

如果对他们进行判断 会发现他们是完全相同的:

这是因为在编译的时候,编译器会将它们两者都编译为System.String,两者都对应着System.String

所以在定义字符串的时候完全可以混用两种写法。

但还是建议使用小写写法,小写是C#基本数据类型的书写习惯

隐式类型变量——var

var关键字定义的变量

C#编译器在编译的时候会依据右值来推断左值的类型

我们把这种方式定义的变量称为隐式类型变量

主要目的是精简代码

比如定义下面的变量的简化

dynamic——变色龙

C# 4.0引入了一个dynamic类型,它是一条变色龙,可以动态地给它添加数据成员,如下所示:

dynamic实际上定义了一种“动态”对象,它支持在运行过程中动态地添加新成员,一切是在“运行”时发生的。

而var只是你代码时可以不指定类型,其实它的类型是明确的,这个类型的成员也是固定的,由它所接收的值所决定。这一切,都是在“编译”时确定。

当前可以对dynamic类型“不求甚解”,知道有它就好。

变量与内存

返回的是(单位:字节)

在C#中较低精度/较小范围类型到较高精度/较大范围类型的转换赋值是可以的,比如

但是反过来从较高精度/较大范围类型到较低精度/较小范围类型的转换时会报错提示无法隐式转换

所以如果需要强制类型转换

一个特殊的点:

数据类型转换

字符串"100"向数值转换:

C#基类库中的一个类也可以实现这个功能:

反过来希望一个数据转换为字符串

ToString方法很通用、几乎所有的原始数据类型都有这个方法

另外一种转字符串的方法是

虽然后者是完全可用的但是还是建议使用ToString

C#变量两种类型

从内存模型角度,C#变量可分为以下这两种类型

线程堆栈/堆(一种特殊的内存区域)

这两种都是特定的内存区域,有不同的特点。目前只需要知道大致分为两种类型

运算符与表达式

与c一样,除号两边都是整数的时候是按照整除的方式来处理的,是商。

如果给其中一个加上后缀d或者.0都是可以的

加减乘除都一样 递增递减一样

选择结构与逻辑表达式

三种典型的程序代码执行流程

在结构化程序设计中,经常使用一种称为“ 程序流程图 ”的示意图 来表示程序的执行流程 。

虽然在面向对象时代,流程图用得没有以前多了,但由于其拥有直观性强的优点,仍然被广泛使用 ,比如在行业应用软件系统开发时 ,使用流程图表达业务流程的处理步骤,几乎所有人都能看得懂。

if/else选择结构

区分关键字与标识符

用于构建逻辑表达式的运算符

运算符说明
>大于,实例:“5>10”,值为“false”
<小于
==等于,实例:“ 9== 100”,值为“false”
>=大于等于
<=小于等于
!=不等于,实例:“100 != 101”,值为“true”

“=>”不是“等于大于”,它在C#程序中有特定的含义,它表示一个Lambda表达式,也可以用于标识“表达式成员”,我们将在后面的课程中对这两种情形进行介绍。

逻辑表达式的组合

使用“&&”组合两个表达式,只有两个条件都满足(都为“true”)时,整个条件才算满足,可以将“&&”与汉语中的“并且”对应上。

&&(And /与)、||(Or/或)、!(Not/非)

与或非

假设A和B都是一个逻辑表达式,对其使用与、或、非组合运算的规律如下:

  1. A && B:只有A和B都为true,结果才为true
  2. A || B:只要A和B中有一个为true,结果就为true
  3. !A:结果总是与A“相反”,比如A为true,则!A为false

多值选择结构 switch_case

为了保证一个值执行一个分支,每个分支后都应该加一个break;语句(最后一个分支可略)

循环

do/while循环的特点:
先做事,再判断循环条件是否满足,满足就继续执行循环体。

所谓“循环结构”,其实就是在特定的场景下,某些语句将被反复地执行多次。

while/do循环的特点:
先判断循环条件,满足了再做事。

当需要执行次数不定的循环时,使用do/while或while/do循环是最自然的选择。

当需要执行次数固定的循环时,使用for循环就变成了最自然的选择。

执行顺序为 ①[int i = 0] ②[i<= 100] ③[{}内代码] ④[i++]

break:提前结束循环,后面还没有执行的循环也不再执行。

continue:提前结束当前循环(本轮循环中还没有执行的代码不再执行),后面的循环继续执行。

“永远不结束”的“死循环”
在某些场景,无法预知到底要执行多少轮循环,也不知道要运行多长时间

在这些场景中,使用“死循环” 是合理的

使用循环结构访问数据集合

在面向对象的软件中,我们会经常遇到“数据集合”这一概念。 “数据集合”,顾名思义,就是“数据的集合”,在实际开发中,有两种类型的数据集合我们经常会遇到……

  1. 保存int、float等值类型数据的集合,如“List<int>”
  2. 保存string和自定义类等引用类型数据的集合,如List<MyClass>,又称为“对象集合”

要遍历这两种类型的数据集合,我们可以使用foreach循环……

数据集合的“遍历”

所谓“遍历”,换一个说法,就是“逐个地访问”。

遍历数据集合最自然的方式,就是使用foreach循环。

使用foreach 遍历数据集合时,不要向集合中增删数据。

各种类型的循环语句都是等价的,可以把一种类型的循环语句用另一种类型的循环语句替换而不会影响到程序的功能。

从表面上看,foreach与前面介绍过的for, do/while等循环并没有什么区别,但实际上,两者是有很大区别的。

C#编译器在编译foreach语句时,会将其转换为使用IEnumerable与IEnumerator的相应代码,并且C# 8还引入了序列和异步循环迭代等新特性,这些内容,当前暂不介绍,而安排在后继的其他课程中,等你掌握了相应的知识之后再学习。

控制台应用程序编程小技巧

当我们编写控制台应用程序时,经常需要知道用户是否按了某些特殊的键(比如F1键),于是问题出来了……

(1)我怎么知道用户按的是哪个键?
(2)怎样编程来检测用户的按键?

当一个控制台程序正在运行时,默认情况下,用户可以使用“Ctrl+C”或“Ctrl+Break”强制中止它的运行。

要禁用“Ctrl+C”的功能,可以设置以下属性

Console.TreatControlCAsInput = true;

从语句到方法

在实际开发中,我们经常会发现某些功能在很多程序中都需要。当然,你可以直接地在不同程序中“Copy & Paste”代码,但这么干,麻烦很多:

当你发现了这些代码中有错误时,你必须找出它们被复制过的所有地方,一一更改,这实在太烦人了……

能不能把这些需要重复使用的代码“归作一堆”,给它起个名字,然后在需要调用它们时,只需指定一个名字即可?

把多个语句组合在一起,共同完成一个功能,向外界返回一个结果,再给它起个名字,这样的一个“代码集合”,在面向对象编程领域,称之为“方法(method)”。

在结构化编程领域,面向对象中的“方法(mothod)”被称为“函数(function)”,这两个术语经常混用,可以看成是一回事(虽然有细微的差别)。

在C# 9.0之前,所有方法都必须放到一个“类(class)”中,不存在完全独立的方法,9.0之后,允许你定义一个不放在类中的方法并立即调用它(还包括变量声明、类的定义、普通语句等),这个特性,称为“Top level Statements(顶级语句)”。

调用Add()方法时传入的“100”和“200”,称为方法的“实参(实际参数)”。

注意:C#编程语言所定义的方法名字,首字母大写,其它许多编程语言,比如Java,习惯是小写字母开头,使用不同的编程语言,需要遵循相应的“惯例”。

注意上面代码的static关键字,它表明这个方法是一个“静态方法(static method)”。

C#中,位于同一个类的静态方法可以通过方法名直接调用,其它类要调用时,需要加上此方法所在的类名,比如:Program.Add(100,200);

如果定义方法时没有加上static关键字,它表明这个方法是一个“实例方法(instance method)”,这种方法依附于特定的对象,外界需要通过对象变量来调用。这部分内容,留待后面课程介绍。

方法的重载

在同一个类中,我们可以定义名字一样的方法,只要它们的参数列表不一样就行了,这种语法特性,叫作“方法的重载(method overload)

只需要达成以下条件之一,就可以构成方法重载的关系

  1. 参数个数不一样
  2. 参数个数相同,但相同位置的参数,其类型不一样

返回值类型不作为方法重载判断的依据。

对于重载方法,到底调用的是哪个,是由其参数决定的。

一个实例:图片浏览器

窗体内有如下元素:

代码:

对openFileDialog1按下"转到定义" 可以看到

而DialogResult是一个枚举

将这个方法整理成函数:

递归

递归概述

递归就是“自己调用自己”

但是上述代码执行后会导致报错

小知识:堆栈溢出(Stack Overflow)

程序代码其实是由“线程(thread)”负责执行的。

操作系统在创建线程(thread)时,会给每个线程配套一块内存区域,线程可以用这块区域存储一些数据。

这块内存区域被称为“线程堆栈(thread stack)

线程堆栈有容量限制,当一个线程要保存的数据超过了这个容量时,就发生了“堆栈溢出

“递归(recursive)”的算法、

An algorithm is called recursive if it solves a problem by reducing it to an instance of the same problem with smaller input.

一个递归的算法,会将一个难以处理的“大”问题的 “规模”分多次地持续压缩,一直持续到压缩后的问题规模小到可以处理为止。其过程往往体现为代码要处理的数据量或计算量在递归前后不断“递减”。

代码:

小结:递归编程的“套路”

  1. 每个递归函数的开头一定是判断递归结束条件是否满足的语句(一般是if语句)
  2. 函数体一定至少有一句是“自己调用自己”的。
  3. 每个递归函数一定有一个控制递归可以终结的变量(通常是作为函数的参数而存在)。每次自己调用自己时,此变量会变化(通常是变小),并传送给被调用的函数。

小结:递归的特点

  1. 先从大到小,再从小到大。
  2. 每个步骤要干的事情都是类似的,只不过其规模 “小一号”。
  3. 必须要注意保证递归调用的过程可以终结,否则,将导致“堆栈溢出(stack overflow)”。

下面我们再来看看另一种编程技巧——递推。

“递归”是“由后至前再回来”,要求第n项,先求第n-1项,……,倒推到前面的可计算出的某项,然后再返回。

“递推”是“从前到后”,先求第1项,然后,在此基础上求第2项,第3项,直至第n项。

结论:递归与递推(iterative )是等价的。

关于递归,再多说几句……

• 在软件科学中,递归这种思想有着重要的应用,比如,许多计算机算法都是用递归实现的。
• 在具体的软件开发实践中,递归也用得非常多。
• 对于初学者而言,要一下子理解递归比较困难,只能在开发实践中慢慢体会,最终方能灵活地应用递归解决实际问题。

除了递归之外,还有一个非常重要的编程技巧是需要掌握的,那就是“回调”。回调在“多线程编程”场景中,还有在诸如JavaScript这样的编程语言写的程序中,被大量地使用。

大数与浮点数的编程技巧

比如在上一个递归的例子中计算!50就会出现一个非常大的负数

问题的根源:计算机能表示的数是有范围的!

C#中int类型的数值占32位,是有符号的,最高一位是符号位,表示“正负”

正数:000……0 ~ 011……1 即 32个全“0”到 “0”+31个全“1”
负数:100……0 ~ 111……1 即“1”+31个全“0” 到 32个全“1”

类似地,long类型的数值占64位:

示例程序计算阶乘出现错误的原因

由于计算机使用固定的位数来保存数值,因此,能处理的数值大小是有限的,当要处理的数值超过了这一范围时,计算机将会自动截断数值的二进制表示为它所能处理的最多位数,这将导致错误的处理结果。

如果我们的确需要处理很大的整数,其数值大到甚至超过了long类型的最大值263-1,是不是就没有办法了呢?

处理巨大的整数

.NET 基类库中提供了一个BigInteger类,支持大整数的加减乘除运算。

注意:BigInteger类型定义于System.Numerics中,需要为.NET Framework项目添加对这一程序集的引用,而.NET Core项目则可以直接使用。

浮点数的处理技巧

计算机不能精确地表达浮点数(特殊形式的除外),因此,当需要比较两个浮点数是否相等时,应该比较其差的绝对值是否在某个允许范围之内即可,无法做到像数学那样的精确比较。

形形色色的方法参数

输出空格或分隔线

在开发.NET控制台程序时,经常需要使用Console.WriteLine()方法输出信息。依据实际情况,有些时候可能希望在连续多个输出中插入一些空行,有些则希望能使用分隔线……

分隔线与空行,都需要使用单独的Console.WriteLine()语句输出,很不方便……

让我们编写一个函数,在输出一个字符串时,可以选择自动添加前后分隔线和空行的功能。

注意一下PrintMessage方法定义了三个bool类型的参数,并且都给它们指定了默认值

指定了默认值的方法参数,在调用时是可省的,所以又被称为“可选参数”。

调用PrintMessage方法时,可以传入一个或多个参数,未传入的参数,取方法定义时指定的默认值:

当一个方法中有很多个参数,在调用时可以通过名字直接给特定的参数传值,而不需要按照定义时的位置顺序,这个称为“命名参数赋值特性”

可变参数

当给一个方法参数前面添加一个params关键字时,就表示它是一个可变参数:

调用示例:

可变参数的类型,必须是一维数组。

当方法有多种参数的时候,可变参数必须在最后面。

“输入(in)”型方法参数

正常情况下,在方法内部,是可以修改传入参数的值的:

当一个参数是in类型时,在程序运行时,方法内部访问到的变量,与外部传给它的变量是同一个,其中不存在值复制的情况。

推荐对于那些包容较多成员的struct变量启用in特性

如果方法参数前没有in,则方法接收到的,是外部number变量值的一个“副本”,两者是独立的变量。

in也有助于提高性能,因为不需要再进行一次复制的操作。对于有相当多变量的时候很有用处。

“输出型(out)”方法参数

.NET基类库中,为int类型定义了一个TryParse方法,其声明如下:

与输入型参数类似,使用输出型参数时,方法内部外部代码中所访问的变量,其实是同一个。

out的意思其实就是“这个实参传进去了,被改变后我仍然需要让这个数再出来

我们可以很方便地为自己的方法添加out关键字,如下所示:书签

使用例如下:

使用输出型参数,可以直接将特定的数据处理任务的结果直接放到“外界”指定的“存储区域”,无需进行数据的复制工作。

另外,out类型的方法参数,还在Deconstruct这一特性(后面介绍)中起到了关键的作用。

输出型参数的简写

早期版本的C#代码,在使用输出型参数时,必须在方法外部单独定义好变量,再把它们传给方法,如下所示:

C# 7之后,可以将变量定义与方法调用“合二为一”

“被丢弃(discard)”的参数

当调用包容有输出型参数的方法时,如果对于特定的参数“不感兴趣(用不到)”,可以直接传给它一个下划线:

在方法调用时传入的“下划线”称为“discard”,它不是一个变量,因此,在后面不能使用它。它仅仅只是一个用于通知编译器的“标记”而己,告诉编译器:“这个参数后面没有用到”。

本地方法

本地方法,是指那些定义在一个方法内部的方法,它们仅在方法内部使用,不能被外部方法所调用。

本地方法可以访问并修改外部方法的本地变量

doSomething方法执行完毕之后,它对value变量的修改仍然是生效的。

闭包(closure):

一个本地方法使用了外部定义的变量,它与它所访问的变量构成一个闭包。当其执行结束之后,方法对外部变量的修改结果可以保存。

静态本地方法

使用静态本地方法,可以避免创建内部匿名类型实例的开销,付出的代价就是它不能访问外部方法中所定义的局部变量,这让其的使用场景受限。

本地方法的应用示例-1

通过将特定的功能代码封装为本地方法,可以让代码更为易读,因为本地方法实际上干的事情,就是将代码 “分块打包”。

将所有计数相关的代码都打包为一个整体。
注意,这是一个“递归”的本地方法。

本地方法的应用示例-2

方法体中出现了重复的代码,可以将其抽取为本地方法,以便消除这些重复的代码。

本地方法的应用示例-3

因为使用了本地方法将相关代码“分块”打包,并且起了易懂的名字,现在你看这个包容有数十行代码的方法,它要干的事情是不是特别地清楚,只需要扫一眼就知道了?

小结:

当你发现一个方法中包容太多的代码,或者是这些代码包容着非常类似的大同小异的逻辑,就可以使用本地方法将其重构。从而让代码易读且逻辑清晰。

本地方法不能被外界调用,很好地贯彻了信息隐藏的原则。

Windows Forms软件开发技术基础

使用Visual Studio编写GUI程序

什么叫“GUI”程序?

GUI = Graphic User Interfce(图形用户界面接口)

GUI程序就是拥有“可以看得见”的窗体的应用程序,你可以使用鼠标和键盘去操作它,我们日常所用的Word、QQ之类,都属于这个类别。

这种程序,通常对应着一个“图标(icon)”,人们习惯于将常用的应用程序的图标摆放到Windows的桌面上,因此,人们又将它们称为“桌面应用”。

两种类型的桌面应用:

VS2019编写 GUI程序 的技巧

不同的项目类型有不同的应用场景,要用C#开发桌面应用,选择“Windows窗体应用程序”这个项目模板。

注意:Windows窗体应用程序(简称WinForms)有两种运行环境,.NET Framework与.NET Core,新写的程序建议使用.NET Core的。

关于代码的折叠:

窗体文件的构成:

关于:有两个窗体时程序运行如何默认打开第二个

在Program.cs内修改:

关于:自定义程序的图标

扩充知识:RAD(快速应用开发)模式

使用Visual Studio编写桌面应用程序,具有 “所见即所得(WYSIWYG:What You See Is What You Get)”的特点,这种开发方式,被称为“RAD(Rapid Application Development)”。

采用RAD方式开发,软件用户能很快地看到可以一个可以跑起来的程序从而给出他的意见,这样一来,软件开发者就能尽早了解到软件是否符合用户的需求,及时调整,开发出“真正满足用户需要”的软件。

RAD开发方式,最适合于规模小,功能简单的带有“演示”性质的程序。

常见WinForm控件的使用方法

按钮控件

“按钮(Button)”是一个“控件(Control)”,它拥有这样的特性:

  1. 当用户用鼠标点击时,触发一个Click事件,如果程序员事先为这个Click事件写好了代码,那么这些代码将被计算机执行。
  2. 如果用户始终没有用鼠标点击按钮,Click事件将不会触发,对应的,程序员事先为这个Click事件写好的代码也永无被被计算机执行的机会。

以上为 事件驱动的程序运行方式

按钮的图片:

标签控件

设置BackColor属性值为Transparent,标签将不带有背景颜色。

在程序中使用标签动态显示信息:

编程要点:控件名字与事件

文本框控件

文本框控件主要用于供用户输入,它是TextBox类的对象

问:怎样得到用户输入的字符串?
答:使用它的Text属性

问:如何即时响应用户的输入?
答:响应它的TextChanged事件

在事件名上右击,选“重置” 命令,可以移除本事件的响应代码

需要牢记:

特定的控件,在不同的场景,会触发不同的事件。

在实际开发中,必须特别注意以下三点:

  1. 控件触发了哪些事件?
  2. 这些事件触发的顺序是怎么样的?
  3. 我选择哪些事件进行响应?

进度条控件与小闹钟控件

进度条:

一个进度条,点击旁边的按钮会使其进度+10,满了之后会清空

时钟控件:

个人实例练习:

时钟软件 https://lingyun67.cn/alarm-clock-demo/

使用容器控件布局窗体(WinForm)

控件的通用属性

控件的激活与禁用:Enabled属性,通过设置其值为True或False,可以激活或禁用特定的控件。

显示与隐藏:visible属性

Anchor属性:响应窗口的大小改变

Dock:黄色的是容器 Dock属性设置之后 按钮会按选项分布在背板上面

控件容器

控件容器是一种特殊的控件,它可以容纳多个“子”控件。

我们己经非常熟悉的“窗体(Form)”,就可以看成是一个容器控件,只不过它是“顶层容器”。

注意:“顶层容器”无法再放入另一个“顶层容器”中。

最简单的容器控件——面板(Panel)

面板中可以放置其他控件,甚至再嵌套一个面板。

使用面板,可以把它所包容的所有控件看成是一个整体,统一地控制它们(比如激活或禁用它们)。

组合框( GroupBox )

组合框功能与面板类似,仅在外观上有所区别。

组合框拥有一个标题和边界,在实际开发中多用于给控件分组。

特别地,当需要在窗体上放置独立的几组“单选钮”时,就可以将它们放在分组框中。

选项卡控件(TabControl)

选项卡控件(或称为多页面控件)能够充分重用有限的屏幕空间,在运行时由用户选择显示哪一组控件。

在程序运行时,我们可以动态地添加或移除选项卡,激活特定的选项卡,在需要的时候,也可以动态地向特定选项卡中添加或移除控件。

设计时添加与移除选项卡:TabControl的TabPages属性包容所有选项卡对象

在设计时可以使用集合编辑器添加或移除选项卡。

分隔条面板(SplitContainer)

FlowLayout控件布局容器

可以动态地添加与移除控件到FlowLayout中,由它负责排列控件。

使用FlowLayout称为“流式布局”,类似于Web网页布局方式,默认情况下,从左到右,从上到下排列其子控件,如果“自动换行(WrapContents)”特性打开,一行摆放不下时,会到下一行排列。如果打开了“自动滚动(AutoScroll)”特性,会在合适时候自动地添加水平或垂直滚动条

TableLayoutPanel

———— C#面向对象编程入门篇————

类和对象(C#)

理解类和对象的概念

“主窗体”和“从窗体”都是窗体,但“地位”不一样。主窗体一关闭,整个程序结束、从窗体关闭,整个程序仍然会继续运行。

学会编写类

所有代码放在类中,类是编程的基本单元。

一个.cs文件包含多个类

C# 9.0之后,代码可以不放到类中,这是C#引入函数式编程特性的结果。

C#使用class关键字定义一个类。类中常见的成员有:

字段和方法组合起来起个名字,就变成了类

C#中的字段与方法,可以加上“publicprivateprotected”关键字控制其存取权限。

(公有的、私有的、被保护的)

需要注意的是:在方法体内定义的j是局部变量,类中的同名j是失效的(变量的屏蔽原则)

  1. 类中的方法,可以直接访问类中的字段。
  2. 类中的方法定义的局部变量,将屏蔽掉类中的同名字段。
  3. 有两种最基本的数据存取权限:

实践建议:在设计一个类时,仅仅只有需要被外界访问的成员才设置为public的。

类的“属性(property)”

字段+get/set方法=属性

使用例:

实现自定义属性的要点:

  1. 定义一个私有字段用于存储属性数据。
  2. 设计一个get方法,当读取属性值时,向外界返回私有字段的当前值。
  3. 设计一个set方法,当向属性赋值时,其自动隐含的value参数保存外界传入的值,应将此值传给前面定义的私有字段。

属性的经典实现方法的弊端:

当一个类中存在有很多的属性,而这些属性又采用类似的方法编写时,这是一个烦人的工作,要敲很多的代码!

C# 3.0的改进——自动实现的属性

“属性”比“字段”多了哪些东西?

当使用经典写法时,我们可以很容易地在读/写属性时“插入一些”特定的代码,完成诸如“检验数据”、“显示信息”、“触发事件”等工作,这是单纯的字段所不具备的特性。

定义一个属性时,get/set方法并不需要同时存在,它们的存取权限也可以不一样,各种情况的不同组合,将影响到属性的存取特性……

开发建议

在实际开发中,多用属性少用字段,尤其是杜绝“公有(public)”字段,因为过多的公有字段,向外部暴露了内部的数据结构,外界可以随意修改它,有可能引发开发者意料之外的Bug,损害了代码的健壮性和可维护性。

表达式体属性(Expression-bodied property)

注意区分:

表达式体方法(Expression-bodied method)

表达式体方法和属性,统称为“表达式体成员(Expression-bodied Member)”,这些特性从C# 6开始引入,以后版本不断地完善。

“表达式体成员”是个语法糖,它与后面课程要介绍Lambda表达式,虽然样子很像,但不是一回事,注意区分。

类的初始化过程

当我们通过new关键字创建一个对象时,一个特殊的函数被调用,此函数被称为——构造函数(构造方法)

所谓“构造方法”,就是在创建对象时被自动调用的方法。

构造方法长成什么样?:

构造方法与类名相同,没有返回值。

一个类可以有多个构造方法,这些构造方法构成“重载(overload)” 关系。在程序实际运行时,依据参数决定调用哪个构造方法。

构造方法主要用于在创建对象时给它的相关字段一个有意义的初始值。

定义一个类时,即使你没有显式地定义一个构造方法,C#编译器也会“偷偷”地给你的类加上一个没有参数的“缺省(或称为默认)构造方法”。

创建对象并给其字段/属性相应初始值的基本方法

以下代码创建MyClass的对象并给其字段或属性赋值:

当一个类定义了多个字段或属性时,代码变得很冗长……

改进方式一:给类添加构造函数

使用代码:

但是存在的问题就是如果我只想给部分属性和字段一个有效的初始值,需要提供多个重载的构造函数。这就让类的定义变得复杂了。

改进方式二:使用C#3.0所提供的“对象初始值设定项”特性,不需要给MyClass添加重载的构造函数即可实现相同目的:

类的原始定义不变

简化后的字段/属性初始化代码

还可以直接初始化集合对象

直接初始化基本数据类型的集合对象

初始化包含引用类型元素的集合对象

 类的组织与管理

分部类与分部方法

分部类(partial class):

把同一个类的代码分散到多个文件中

分部方法(partial method):

在一个文件中声明方法,在另一个文件中实现或调用方法

分部方法与分部类的应用实例:

命名空间

把类比作书的话,命名空间就是书架

命名空间(namespace):可以看成是类的仓库,.NET中所有的功能都由类提供,这些类被分门别类地存放在特定的命名空间中。

有单独的命名空间

也有多层嵌套的命名空间

要点:

在C#中创建和使用命名空间:

C#中使用namespace关键字来定义命名空间

使用using语句来引用命名空间的类

使用“对象浏览器”

命名空间在“对象浏览器” 中使用“{}”展示程序集中的命名空间及其包容的类型

C# 6中的简化

对于一些完整名字很长的类型,可以通过提前“静态导入”的方式,大大缩短其长度,从而允许在代码直接写上方法名。

程序集

如果把程序集比作砖块,那么应用程序就是建筑物

关于“程序集”应该知道的……:

  1. .NET程序的基本构造块是“程序集(Assembly)”。
  2. 程序集是一个扩展名为.dll或.exe的文件。
  3. .NET 中的各个类,存放在相应的程序集文件中。

如何创建一个程序集?

“类库(class library)”项目模板可以用于创建一个DLL程序集

使用程序集

在一个新项目中添加对于特定程序集的“引用(Reference)”,即可使用此程序集中的类:

程序集与命名空间的关系

  1. 程序集的物理载体是“实实在在可以看得到”的.dll或.exe文件。
  2. 命名空间是类的一种组织方式,它是一个逻辑上的概念。一个命名空间中的类可以分布在多个程序集文件中。
  3. 一个程序集至少包含一个命名空间。可以在项目的“属性”面板中直接指定其生成的程序集默认的命名空间(如下图所示)。

基于程序集开发

在早期的.NET程序中,通常都是直接引用本地程序集的物理文件(.dll或.exe)来重用其内部的代码,后来,为了方便组件的跨互联网重用,以及解决版本管理和组件间依赖的问题,引入了一种新的组件——Nuget包,并且慢慢地成为了主流。
有关Nuget包的相关介绍,参看“.NET Core 软件开发技术导论与自学指南”课程中的相关章节。

积木式的软件开发方式

基于程序集,可以方便地在.NET平台上实现组件化开发,其具体过程为:

  1. 重用已有的组件
  2. 开发部分新的组件
  3. 将新老组件合在一起“搭积木”。

从现在开始,当开发正式的项目时,都应该采用基于程序集的开发方式!

“对象”与“对象变量”那些事儿

对象与对象变量

对象变量与内存模型

“对象变量”与“对象”之间的关系……

对象生存于托管堆(Managed Heap)中,当不用时,CLR会自动回收其内存。

CLR中有一个垃圾回收的一个线程,在合适的时候他会运行,检查哪些对象已经不再使用然后回收其所占用的内存

对象生成的这块区域:托管堆的管理不是程序员负责,是由虚拟机 CLR负责

方法中所定义的对象变量保存在线程堆栈(Thread Stack)中,这两者是不一样的

线程堆栈 vs. 托管堆

  1. 程序代码其实是由线程负责执行的,每个线程都拥有一个用于保存临时数据的特定内存区域,称为“线程堆栈(Thread Stack)”。
  2. 保存在线程堆栈中的数据,当它所关联的线程运行结束时,这个线程堆栈会被销毁,导致其中的数据“全没了”。
  3. 保存在托管堆中的数据,只有当整个程序结束时,才会被全部“销毁”。

C#中的两种主要的变脸类型:引用类型 vs. 值类型

“类”类型的变量属于“引用类型(Reference Type)”,其引用的对象占用的内存位于“托管堆(managed heap)”中。

int之类简单类型(还包括struct等)的变量属于“值类型(Value Type)”,方法内部所定义的值类型的变量,其占用的内存位于“线程堆栈(thread stack)”中。

一个思考:假设MyClass是一个类,请看以下C#代码:

上述代码执行之后,obj1和obj2引用的是两个不同的对象吗?

对象变量“相互赋值”的真实含义

首先会在托管堆中new一个MyClass对象,紧接着定义好了一个对象变量obj1,这个对象变量负责引用这个MyClass对象,对象变量obj1的内存区域保存的是MyClass对象在托管堆中的地址值

可以说对象变量obj1是一块用来保存地址的存储区域

第二句 MyClass obj2 = null; 定义了一个新的对象变量obj2=null,null是一个特殊值,表示obj2是一个空引用

当obj1赋值给obj2,obj1没有改变

要注意的是,对象变量的赋值其实是对象变量所关联的内存区域地址的值复制

赋值之后obj2和obj1的保存的值是一样的,所以他们引用相同的对象,在全部过程中,MyClass对象都只有一个。

对象判等

一段代码:

两个值类型变量的判等

int类型其实是一个结构体,这个结构体里是.NET基类库在程序集mscorlib中封装的基本的数据类型,是int32,其中有很多相应方法,比如Equals

Equals方法的摘要是比对int32这个结构体里面的数值是否是一样的,返回一个bool类型

这段代码运行的结果是两个True

另一段代码:

两个对象的判等

这个Equals方法与之前的来源不同,这个方法来自object类型

当定义一个类的时候,没有指明他的基类库,那么他的基类库默认为Object。

基类库的作用是完成框架的通用性开发而必须的基础类和常用工具类等

代码的输出结果是两个False,这表明obj1与obj2引用了两个不同的对象,他们内存的地址不同equals方法默认的功能和==一样,但有的时候我们希望比对两个对象的字段值(按照内容进行比较而不是按照引用来比较)

此时就需要重写基类的equals方法

在MyClass内对Equals进行override

代码运行后输出的是False True

因为第二次的比对使用覆写的Equals方法判断的是字段值

override 方法:提供从基类继承的成员的新实现 也就是覆写 重写

两个字符串的判等

代码输出的是两个true

注意:string类型是引用类型,但是它的等于号和Equals方法是一样的,都是判断这两个字符串的值是否是一样的

string类型 对判等==运算符 进行了重写

注意:此处的结论仅适用于C#,另外的编程语言比如Java,仍然严格区分string类型的==和Equals方法,==比较对象引用,Equals比较对象内容。

结论:

  1. 当“==”运算符施加于两个值类型变量时,实际上是比对两个变量的内容(值)是否一样。
  2. 当“==”运算符施加于两个引用类型变量时,实际上是比对这两个变量是否引用同一对象!
  3. 要“按值比较”对象,需重写其Equals()GetHashCode()方法。
  4. String是引用类型,但它的“==”经过了重写,其功能与“Equals()”方法一样,都是比较两个字符串的“内容”是否一样

另一个比较对象引用的方法:

.NET Core和.NET Framework中,都为Object类(它是所有.NET类的最顶层基类)定义了一个ReferenceEquals()方法,可以使用这个方法来比对任意两个对象变量是否引用同一个对象。

this引用

类的实例方法可以直接访问同一个类的实例字段,其中隐藏着一个this引用

C#中的this,是一个特殊的对象引用,它代表对象自身。

This = me

位于同一类内部的成员彼此访问,本质上是通过this这一特殊引用来完成的。只不过这个关键字通常被省略了。

通过对象变量来访问对象的实例成员,是面向对象编程的一个基本准则。

装箱与拆箱

C#是一种“强类型”的编程语言

如果把值类型数值赋给引用类型变量:

会在托管堆中创建一个对象 值是num,然后让obj指向这个对象,这个过程的术语就是装箱

对象赋值给值类型变量:

把对象里面所包容的数值取出来、然后再把它赋值给一个值类型的变量。这个过程就是拆箱

这种装箱/拆箱的特性是面向对象特有的,使用的并不多,会对程序的性能造成影响

方法参数传送方式与ref

两种类型的方法参数

就是一个是复制值过去,一个是传地址过去,跟指针一样

按“引用方式”传递值类型参数

返回ref的函数

不仅可以将方法的参数定义为ref的,甚至连方法的返回值,也可以是ref的!

为此,我们设计一个实例,展示返回ref结果的方法是什么样子的。

编写一个工程师类

只读的数据类型

通常情况下,对象的字段值是可以修改的

但是在.NET基类库中,我们发现

值类型的DateTime不太一样:

字符串类型也不太一样:

DateTime和string类型变量居然都是只读的!一旦创建之后,内容不可改!

为什么要设计“只读”的类?

所以,在“多线程”环境下,使用只读对象可以提升程序的性能。

设计“只读”的类

当外界期望修改对象的字段值时,不是修改原有对象的字段值,而是新建一个对象,让它的字段值符合要求,然后把这个新对象返回给外界!

使用例如下

readonlyObj2 == readonlyObj 为false 他们引用不同的对象

C# 7以后,允许定义只读的结构体:

如果编译器发现程序中任何的地方有代码尝试修改只读结构体的字段,就会报告编译错误,从而将“错误消灭在萌芽状态”。

只读的结构体方法

在C# 8中,允许你将结构体中的某个方法定义为只读的,编译器会检查这个方法内部有没有代码修改了外部的数据(比如,修改了结构体中的某个字段的值,或者是调用了另一个“非只读”的方法),如果发现了这种情况,编译器就会引发一个“编译错误”,提醒你修正它。

小结:

类的静态成员

在实际开发中,我们可能会有一些“到处都要使用”的功能需要实现,比如各种标准的数学函数以及圆周率等数学常量,在C#中如何定义并实现它们?

.NET基类库中Math类所封装的部分数学常量与函数

使用const定义数学常量

使用static定义数学函数

使用static关键字来定义类的静态成员

两种类型的类成员

静态(static)方法/字段/属性:使用static定义

实例(instance)方法/字段/属性:不使用static定义

实例的类成员想要访问必须要通过一个对象变量,静态的对象成员不需要对象变量,只需要一个类名就可以

类的静态方法可以访问类的静态字段,但是不能直接访问类的实例字段

相应的,实例的方法可以访问静态字段,也可以访问实例字段

另一个实例:

每次创建都各给两个字段+1,dynamicVar字段为实例成员,staticVar字段为静态成员

输出时 dynamicVar = 1 staticVar = 100

事实上:

类成员的访问规则:

使用静态成员的好处

  1. 由于静态成员并不依附于特定的对象,而可以直接调用,因此,它使用更方便。
  2. 编写静态方法时,如果它需要访问静态的字段或属性,则要注意在多线程环境下,有不有可能出现数据存取错误的情况。
  3. C#提供有一种非常方便的“扩展方法”,能够在不修改源代码的前提下,动态地给特定的类型添加新的方法,这一特性,就是使用“静态类 + 静态方法”实现的,在后面的课程中会介绍这块的内容。

匿名类型

没有名字的“临时”对象

在C#中,如果你只需要“临时”使用一个对象,并且这个对象只在这个地方使用,那么,你可以使用匿名类型达到这个目的:

user对象内的id name age的类型都是编译器推断的

对于匿名类型,C#编译器会将匿名类型转换为一个类,在程序运行时,以这个类为模板,创建一个匿名对象供程序使用。

所以所谓的没有类型其实只是针对于在编写代码的时候的没有。

将匿名对象序列化为Json

将一个对象属性信息提取出来,生成一个Json字符串的过程,称为对象的“Json序列化”。
.NET基类库中有相应的组件,可以直接完成这个工作:

注意,默认情况下,属性中的“中文字符”,会被转义,这个是Json规范所约定的。

不转义中文字符串:

如果希望汉字能保持原样,可以设置JsonSerializer不对中文字符进行转义:

示例中用到的JsonSerializer是.NET Core基类库中的内置组件,在.NET Core项目中可以直接拿来就用。
另有一个历史悠久的第三方Json库——Newtonsoft.Json,无需任何设置就能在序列化汉字时保持原样不动,但必须单独使用NuGet管理器在线安装后,项目中才能用它。

匿名类型的应用场景-1

匿名类型通常只在方法内部使用,主要用于临时表达一些信息,用完就扔……

比如,在ASP.NETCore Web应用中,就经常使用匿名类型封装客户端需要的信息。

匿名类型的应用场景-2

多窗体编程初步

C#中的数组

数组

我们把一组有顺序的数据所构成的整体,称为“数组”。

数组中数据的位置编号,从0开始依次递数组中数据的位置编号,从0开始依次递

数组中的单个数据,称为“元素”,它们具有相同的数据类型。

数组一旦创建之后,尺寸保持不变,元素在内存中连续分布。

数组是一个对象,数组变量(arr)引用这个数组对象。

通过“数组名[索引值]”访问单个的数组元素。

数组不允许“越界访问”,否则,会抛出一个IndexOutOfRangeException

对象数组

数组中的元素可以是引用类型的,这种数组俗称为“对象数组”。

变量stringArr引用一个包容4个元素的数组对象,每个元素又引用一个字符串对象……

数组本身是一个引用类型,因此,如果它被传送给一个方法,则方法内部对数组的修改,会直接作用于“原始”的数组对象。

但是被修改的并不是之前的字符串How are you本身,而是数组所引用的对象被修改了,之前的字符串仍然存在,等待被垃圾回收器回收

继承

继承概述

继承是对现实生活中的“分类”概念的一种模拟。

示例:狮子是一种动物。

狮子拥有动物的一切基本特性,但同时又拥有自己的独特的特性,这就是“继承”关系的重要性质。

形成继承关系的两个类之间,是“IS_A”关系

IS_A译为:"是一种"

在C#中实现继承:

首先定义一个Animal类:

Animal类被称为父类(parent class) 或者 基类(base class)

然后定义一个类Lion

Lion类被称为类(child class)

从外部使用者角度看来,子类“自动”拥有了父类声明为publicprotected(保护) 的成员,这就是继承的最重要特性之一。

父类:

子类:

子类中的代码可以直接访问父类保护级别的成员,但外界不能通过对象变量来直接访问声明为保护级别的类成员。

应用:

更进一步:继承环境下的字段访问规则

方法的重载与覆盖

子类对象可以赋值给父类(基类)变量,这实际上是“IS_A”关系的体现。

当子类、父类的方法名相同时,有两种情况:

Overload(重载)

Override(重写/覆盖)

当方法的元素不同时,会出现Overload(重载),相同时则会进行Override(重写/覆盖)

子类父类方法字段“一模一样”时

测试代码:

开发建议:不要自找麻烦!

在实际开发时,不要在子类中定义与父类一模一样的成员(包括字段、属性和方法)!

提升软件开发效率的法宝——重用

在面向对象思想发展的初期,通过继承复用代码曾经被认为是面向对象最重要的目标之一。

很遗憾,实践中人们发现在开发中滥用继承后患无穷……

代码间强耦合,拥有极深的类型继承树,上层基类一改,所有子类均受影响,并且这种变动所带来的影响很难预计……

继承是现实世界对事物“类别”关系的一种模拟,在了解了与继承之间的相关知识之后,请观察一下你周围的事物,它们中的哪些可以应用“继承” 构建出一个面向对象的软件模型?

抽象类与接口

抽象类与抽象方法

在一个类前面加上“abstract”关键字,此类就成为了抽象类。

一个方法前面加上“abstract”关键字,此方法就成为了抽象方法。

“抽象类”怎么用?

不能创建抽象基类的对象,只能用它来引用子类的对象。

让我们先从“继承”聊起……

“继承”是对现实世界中“是一种(IS_A)”关系的模拟。

现在试着为以下一个场景建立一个面向对象的编程模型

鸭子是一种鸟,会游泳,同时又是一种食物。

• “会游泳”这个方法放在哪个类中?

  1. 并不是只有鸭子一种鸟会游泳。
  2. 并不是所有鸟都会游泳。

因此不知道“会游泳”这个特性应该放到哪个类中

C#/Java等编程语言不支持多继承

解决方案就是使用接口:

接口

C#中接口的特点

使用interface关键字定义接口 接口的名字通常以“I”打头

接口中的方法只有声明,不包容任何代码

经典的面向对象编程语言,最初接口中除了函数或属性/字段定义,是没有其他语言成份的,但近些年来,这条规则被突破,Java、C#纷纷允许在接口中放入有实现代码的方法,后面课程介绍相关的内容。

“接口”小结

理解“多态”

继承多态

“多态性”一词最早用于生物学,指同一种族的生物体虽然具有相同的本质特征,但在不同环境中可能呈现出不同的特性。如东北大米对比泰国香米、各种种类的狗之间对比。

面向对象开发中的多态

在面向对象理论中,多态是:

同一操作施加于不同的类的实例,不同的类将进行不同的解释,最后产生不同的结果。

从编程角度来看,“多态”表现为:

同样的程序语句,在不同的上下文环境中可能得到不同的运行结果。

多态实例:

苹果和菠萝都是一种水果,它们都有“适宜种植区域” 这个信息值得关注。

因此为苹果、菠萝建立面向对象软件模型

水果被定义为“抽象类”,其中定义一个抽象方法GrowInArea(),表示“种植区域”,要求子类必须重写。

苹果和菠萝成为Fruit的子类,分别为其抽象方法GrowInArea()提供了不同的实现代码,这种多态编程方式称为 “子类重写基类的抽象方法”。是一种最常见的多态代码的表现形式。

相同的一句“f.GrowInArea()”,由于 f 引用的对象不同,导致其输出结果不同。

这点就是多态特性的一种体现

“真正的”多态代码:

此方法中的代码只调用“基类” 中定义的方法,不涉及任何具体的子类,因此,此方法里面全部都是“多态” 代码。

多态代码调用实例

ShowFruitGrowInAreaInfo()方法可以输出任何一种水果(比如桔子)的“适宜种植地”信息,只要程序中有相应的派生自Fruit类的特定水果类(比如Orange)即可。

需要扩充的时候,只需要定义一个比如是Orange,然后继承自Fruit即可

多态的代码,只调用“基类”中定义的方法,存取 “基类”中定义的字段和属性,简单地说,就是:

针对“基类”编程

动物园示例

假设某动物园管理员每天需要给他所负责饲养的狮子、猴子和鸽子喂食。我们用一个程序来模拟他喂食的过程。

面向对象建模中的“名词法”

用人类的自然语言描述出软件要干的事,挑出其中的名词,它们就是“候选”的“类”。

描述:

动物园管理员每天需要给他所负责饲养的狮子、猴子和鸽子喂食。

抽取名词:

管理员、鸽子、狮子、猴子、动物园

上面去掉过于宽泛的动物园名词。

使用“名词法”建立软件模型

三种动物对应三个类,每个类定义一个eat()方法,表示吃饲养员给它们的食物。

再设计一个Feeder类代表饲养员,其name字段保存饲养员名字,三个方法分别代表喂养三种不同的动物,其参数分别引用三种动物对象。

喂食过程:

但是不同的饲养员,应该喂食的动物是不固定的。或者可能饲养员每个月饲养的动物也不相同。

因此这样设计Feed是不合理的

重构:引入继承

设计抽象类Animal,定义抽象方法eat(),每个子类都覆写这个eat

重新设计Feeder类的喂养方法

因此喂食过程变成了:

事实上还可以进一步优化

然后我就听不懂了

“多态”的好处

从这个示例中可以看到,通过在编程中应用多态:

可以使我们的代码具有更强的适用性

当需求变化时,多态特性可以帮助我们将需要改动的地方减少到最低限度

“多态”具体实现方式有两种:

C#接口新特性

接口中的静态成员

在C# 8中,可以为接口添加静态的字段与静态的方法。

接口中的静态字段多为只读类型的字段,代表与本接口所代表的事物密切相关的数据。

而静态方法多用于封装一些与本接口所代表的事物密切相关的“公用”代码。

接口中的静态成员,需要通过接口名来访问:

通过接口名,可以非常清晰地分辨出——“此信息归属于拥有哪种特性的事物,此功能是由拥有哪种特性的事物所提供的”。

实现接口的类

实现接口的具体类型,可以使用本接口所定义的静态字段:

放在接口中的静态成员,通常封装了相关的信息和代码,它们与接口所抽象出来的“事物特性”密切相关。

实现了接口的对象,它本身就有接口所定义的这些特性,因此,它使用接口中的静态成员就非常自然,这种编程方式,体现出了“将相关的东西(或代码)集中放置以便于管理和维护”这样一种推荐的做事方法。

接口默认方法

我们也可以在接口中定义一个“普通的”方法:

实现接口的类,如果不提供此方法的自己的实现代码,那就使用接口所定义的,这就是接口所定义的方法为什么被称为“默认方法”的原因。

MyClass和MyOtherClass在定义好之后,使用起来和其他类是没有区别的。

模式匹配

基本语法:

常量匹配模式(constant pattern)

所谓“常量匹配”,就是把一个变量直接与一个具体的数值或对象比较。

比如

就等价于

类型匹配模式(Type Pattern)

所谓“类型匹配”,就是判断一个对象(或数值),是否是某个类型的实例。

在“类型匹配”表达式的后部,可以追加定义一个局部变量,此变量具有本分支所对应的类型,可以用于此分支后继的表达式或语句中。

使用类型匹配构建复杂的逻辑表达式

类型匹配,可用于构建复杂的逻辑表达式:

上述模式匹配表达式所引入的变量x,可以用于构建更复杂的表达式,或者直接用于分支所包容的语句中,省去了进行类型转换的麻烦。

类型匹配表达式,也可以用于switch语句

if和switch语句,是使用模式匹配表达式的主要应用场景。

属性匹配模式(property pattern)

在表达式中的“{”和“}”内部,可以放入多个对象定义的属性,当前版本只支持“判等”运算。

When子句

在模式匹配表达式中,可以添加一个when子句,对本分支的 “判断规则”进行进一步的 “补充”。

when子句主要用于switch结构,

注意分支的排列顺序。如果不小心,有些分支可能永远执行不到。

Switch表达式(C# 8)

可以将整个switch结构转换为一个表达式,并将其结果传给一个变量: