行业知识
【C#图解教程】笔记
文章目录
- 1. C#和.NET框架
- .NET框架的组成
- .NET框架的特点
- CLR
- CLI
- CLI的重要组成部分
- 各种缩写
- 2. C#编程概括
- 标识符
- 命名规则:
- 多重标记和值
- 格式化数字字符串
- 对齐说明符
- 格式字段
- 标准数字格式说明符
- 标准数字格式说明符 表
- 3. 类型、存储和变量
- 数据成员和函数成员
- 预定义类型
- 预定义简单类型
- 预定义非简单类型
- 用户定义类型
- 堆和栈
- 栈
- 堆
- 值类型和引用类型
- 存储引用类型对象的成员
- C#类型的分类
- 变量
- 4. 类
- 类成员
- 字段
- 方法
- 创建变量和类的实例
- 访问修饰符
- 私有访问和公有访问
- 5. 方法
- 局部变量
- var 关键字
- 局部常量
- 局部函数
- 参数
- 引用参数
- 引用类型作为值参数和引用参数
- 第一种情况 将引用类型对象作为值参数传递
- 输出参数
- 参数数组
- 方法调用
- 参数类型总结
- ref 局部变量和 ref 返回
- 方法重载
- 命名参数
- 可选参数
- 栈帧
- 递归
- 6.深入理解类
- 成员修饰符的顺序
- 静态字段
- 成员常量
- 属性
- 访问器
- 属性和字段的命名约定
- 属性与公有字段
- 自动实现属性
- 静态属性
- 实例构造函数
- 默认构造函数
- 静态构造函数
- readonly 修饰符
- this 关键字
- 索引器
- 索引器和属性
- 声明索引器
- 索引器重载
- 访问器的访问修饰符
- 访问器的访问修饰符有几个限制:
- 分部类和分部类型
- 分部方法
- 7. 类和继承
- 类继承
- 屏蔽基类的成员
- 基类访问
- 使用基类的引用
- 虚方法和覆写方法
- 类访问修饰符
- 程序集间的继承
- 成员访问修饰符
- 成员可访问性总结
- 抽象成员
- 抽象类
- 密封类
- 静态类
- 扩展方法
- 8. 表达式和运算符
- 9. 语句
- using 语句
- 包装资源的使用
- 多个资源和嵌套
- using 语句的另一种形式
- 10. 结构
- 类和结构在内存的安排
- 对结构和类赋值的区别
- 构造函数和析构函数
- 实例构造函数
- 静态构造函数
- 11. 枚举
- 设置底层类型和显式值
- 位标志
- 12. 数组
- Clone 方法
- 13. 委托
- 委托使用步骤
- 为委托添加方法
- 为委托移除方法
- 调用带返回值的委托
- 调用带引用参数的委托
- 匿名方法
- 语法:
- 使用匿名方法
- 返回值
- 参数
- params 参数
- 变量和参数的作用域
- 外部变量
- 捕获变量的生命周期的扩展
- Lambda 表达式
- 事件
- 发布者和订阅者
- 源代码组件概览
- 声明事件
- 订阅事件
- 触发事件
- 15. 接口
- 声明接口
- 接口是引用类型
- as 运算符
- 多接口
- 16. 转换
- is 运算符
- 17. 泛型
- 泛型类
- 声明泛型类
- 创建构造类型
- 创建变量和实例
- 类型参数的约束
- Where 子句
- 约束类型和次序
- 约束类型
- 次序
- 泛型方法
- 声明泛型方法
- 调用泛型方法
- 类型推断
- 扩展方法和泛型类
- 泛型结构
- 泛型委托
- 泛型接口
- 泛型接口的实现必须唯一
- 逆变和协变
- 协变
- 逆变
- 18. 枚举器和迭代器
- 枚举器和可枚举类型
- 使用 foreach 语句
- IEnumerator 接口
- IEnumerable 接口
- 泛型枚举接口
- 迭代器
- 迭代器块
- 使用迭代器来创建枚举器
- 使用迭代器创建可枚举类型
- 迭代器的实质
- 19. LINQ
- 匿名类型
- 方法语法和查询语法
- 查询变量
- 查询表达式的结构
- from 子句
- join 子句
- 标准查询运算符
- LINQ to XML
- XML 基础
- XML 类
- 20. 异步编程
- 什么是异步
- async/await
- 异步方法
- await 表达式
- 取消一个异步操作
- 异常处理和 await 表达式
- 在调用方法中同步地等待任务
- 在异步方法中异步地等待任务
- Task.Delay 方法
- Task.Yield
- 使用异步 Lambda 表达式
- BackgroundWorker 类
- 并行循环
- 其他异步编程
- 异步方法调用的三种标准模式
- 21. 命名空间和程序集
- 22. 异常
- 23. 预处理指令
- 24. 反射和特性
- 元数据和反射
- Type 类
- System.Type 类的部分成员
- 获取 Type 对象
- 什么是特性
- 应用特性
- 预定义的保留特性
- Obsolete 特性
- Conditional 特性
1. C#和.NET框架
.NET框架的组成
.NET框架由三部分组成,严格地说CLR(Common Language Runtime,公共语言运行库)和FCL(框架类库)两部分组成,不包括工具。(FCL是BCL的超集,还包括Windows Forms、ASP.NET、LINQ以及更多命名空间)
.NET框架的特点
- 面向对象的开发环境
- 自动垃圾收集:CLR有一项服务称为GC(Garbage Collector,垃圾收集器),它能为你自动管理内存。
- 互操作性
- 不需要COM
- 简化的部署
- 类型安全性
- 基类库
CLR
.NET框架的核心组件是CLR,它在操作系统的顶层,负责管理程序的执行。
CLR还提供下列服务:
- 自动垃圾收集;
- 安全和认证;
- 通过访问BCL得到广泛的编程功能,包括如Web服务和数据服务之类的功能。
CLI
CLI(Common Language Infrastructure,公共语言基础结构)就是一组标准,它把所有.NET框架的组件连结成一个内聚的、一致的系统。它展示了系统的概念和架构,并详细说明了所有软件都必须坚持的规则和约定。
CLI的重要组成部分
-
公共类型系统
CTS(Common Type System,公共类型系统)定义了那些在托管代码中一定会使用的类型的特征。CTS的一些重要方面如下。
- CTS定义了一组丰富的内置类型,以及每种类型固有的、独有的特性。
- .NET兼容编程语言提供的类型通常映射到CTS中已定义的内置类型集的某一个特殊子集。
- CTS最重要的特征之一是所有类型都继承自公共的基类——object。
- 使用CTS可以确保系统类型和用户定义类型能够被任何.NET兼容的语言所使用。
-
公共语言规范
CLS(Common Language Specification,公共语言规范)详细说明了一个.NET兼容编程语言的规则、属性和行为,其主题包括数据类型、类结构和参数传递。
各种缩写
2. C#编程概括
标识符
标识符是一种字符串,用来命名变量、方法、参数和许多后面将要阐述的其他程序结构。
命名规则:
- 字母和下划线(a-z、A-Z 和 _ )可以用在任何位置。
- 数字不能放在首位,但可以放在其他的任何位置。
- @字符只能放在标识符的首位。(虽然允许使用,但不推荐)
多重标记和值
在 C#中,可以使用任意数量的替代标记和任意数量的值。
- 值可以以任何顺序使用。
- 只可以在格式字符串中替换任意次。
例如:
Console.WriteLine("Three integers are {1},{0} and {1}.", 3, 6);控制台输出结果:
Three integers are 6,3 and 6.
格式化数字字符串
例子:
Console.WriteLine("The value: {0}." , 500); // 输出数字Console.WriteLine("The value: {0:C}.", 500); // 格式为货币 ↑ 格式化为货币这段代码产生了如下的输出:
The value: 500.The value: ¥500.00.
两条语句的不同之处在于,格式项以格式说明符形式包括了额外的信息。大括号内的格式说明符的语法由3个字段组成:索引号、对齐说明符和格式字段(format field)
对齐说明符
-
对齐说明符表示了字段中字符的最小宽度。对齐说明符有如下特性。
- 对齐说明符是可选的,并且使用逗号来和索引号分离。
- 它由一个正整数或负整数组成。
- 整数表示了字段使用字符的最少数量。
- 符号表示了右对齐或左对齐。正数表示右对齐,负数表示左对齐。
索引——使用列表中的第0项 ↓Console.WriteLine("{0, 10}", 500); ↑对齐说明符——在10个字符的字段中右对齐例如,如下格式化int型变量myInt的值的代码显示了两个格式项。在第一个示例中,myInt的值以在10个字符的字符串中右对齐的形式进行显示;第二个示例中则是左对齐。格式项放在两个竖杠中间,这样在输出中就能看到它们的左右边界。
int myInt = 500;Console.WriteLine("|{0, 10}|", myInt); // 右对齐Console.WriteLine("|{0,-10}|", myInt); // 左对齐这段代码产生了如下的输出,在两个竖杠的中间有10个字符:
| 500|
|500 |
值的实际表示可能会比对齐说明符指定的字符数多一些或少一些:
- 如果要表示的字符数比对齐说明符中指定的字符数少,那么其余字符会使用空格填充;
- 如果要表示的字符数多于指定的字符数,对齐说明符会被忽略,并且使用所需的字符进行表示。
格式字段
格式字段指定了数字应该以哪种形式表示。例如,应该被当做货币、十进制数字、十六进制数字还是定点符号来表示?
格式字段有三部分,如图2-4所示。
- 冒号后必须紧跟着格式说明符,中间不能有空格。
- 格式说明符是一个字母字符,是9个内置字符格式之一。字符可以是大写或小写形式。大小写对于某些说明符来说比较重要,而对于另外一些说明符来说则不重要。
- 精度说明符是可选的,由1~2位数字组成。它的实际意义取决于格式说明符。
图2-4 标准的格式字段字符串
如下代码是格式字符串组件语法的一个示例:
索引——使用列表中的第0项↓Console.WriteLine("{0:F4}", 12.345678); ↑ 格式组件——4位小数的定点数如下代码给出了不同格式字符串的一些示例:
double myDouble = 12.345678;Console.WriteLine("{0,-10:G} -- General", myDouble);Console.WriteLine("{0,-10} -- Default, same as General", myDouble);Console.WriteLine("{0,-10:F4} -- Fixed Point, 4 dec places", myDouble);Console.WriteLine("{0,-10:C} -- Currency", myDouble);Console.WriteLine("{0,-10:E3} -- Sci. Notation, 3 dec places", myDouble);Console.WriteLine("{0,-10:x} -- Hexadecimal integer", 1194719 );这段代码产生了如下的输出:
12.345678 -- General12.345678 -- Default, same as General12.3457 -- Fixed Point, 4 dec places.35 -- Currency1.235E+001 -- Sci. Notation, 3 dec places123adf -- Hexadecimal integer
标准数字格式说明符
表2-4总结了9种标准数字格式说明符。第一列在说明符名后列出了说明符字符。如果说明符字符根据它们的大小写会有不同的输出,就会标注为区分大小写。
标准数字格式说明符 表
名字和字符 意义 货币
C、c使用货币符号把值格式化为货币,货币符号取决于程序所在PC的区域设置
精度说明符:小数位数
示例:Console.WriteLine("{0:C}",12.5);
输出:$12.50十进制数
D、d十进制数字字符串,需要的情况下有负数符号。只能和整数类型配合使用
精度说明符:输出字符串中的最少位数。如果实际数字的位数更少,则在左边以0填充
示例:Console.WriteLine("{0:D4}",12);
输出:0012定点
F、f带有小数点的十进制数字字符串。如果需要也可以有负数符号
精度说明符:小数的位数
示例:Console.WriteLine("{0:F4}",12.3456789);
输出:12.3457常规
G、g在没有指定说明符的情况下,会根据值转换为定点或科学记数法表示的紧凑形式
精度说明符:根据值
示例:Console.WriteLine("{0,G4}",12.345678);
输出:12.35十六进制数
X、x
区分大小写十六进制数字的字符串。十六进制数字A~F会匹配说明符的大小写形式
精度说明符:输出字符串中的最少位数。如果实际数的位数更少,则在左边以0填充
示例:Console.WriteLine("{0:x}",180026);
输出:2bf3a数字
N、n和定点表示法相似,但是在每三个数字的一组中间有逗号或空格分隔符。从小数点开始往左数。使用逗号还是空格分隔符取决于程序所在PC的区域设置
精度说明符:小数的位数
示例:Console.WriteLine("{0:N2}",12345678.54321);
输出:12,345,678.54百分比
P、p表示百分比的字符串。数字会乘以100
精度说明符:小数的位数
示例:Console.WriteLine("{0:P2}",0.1221897);
输出:12.22%往返过程
R、r保证输出字符串后如果使用Parse方法将字符串转化成数字,那么该值和原始值一样。Parse方法将在第25章描述
精度说明符:忽略
示例:Console.WriteLine("{0:R}",1234.21897);
输出:1234.21897科学记数法
E、e
区分大小写具有尾数和指数的科学记数法。指数前面加字母E。E的大小写和说明符一致
精度说明符:小数的位数
示例:Console.WriteLine("{0:e4}",12.3456789);
输出:1.2346e+0013. 类型、存储和变量
数据成员和函数成员
像short、int和long等这样的类型称为简单类型。这种类型只能存储一个数据项。
其他的类型可以存储多个数据项。比如数组(array)类型就可以存储多个同类型的数据项。这些数据项称为数组元素。可以通过数字来引用这些元素,这些数字称为索引。
预定义类型
C#提供了16种预定义类型,其中包括13种简单类型和3种非简单类型。
所有预定义类型的名称都由全小写的字母组成。预定义的简单类型包括以下3种。
- 11种数值类型。
- 不同长度的有符号和无符号整数类型。
- 浮点数类型float和double。
- 一种称为decimal的高精度小数类型。与float和double不同,decimal类型可以准确地表示分数。decimal类型常用于货币的计算。
- 一种Unicode字符类型char。
- 一种布尔类型bool。bool类型表示布尔值并且必须为true或false。
说明 与C和C++不同,在C#中的数值类型不具有布尔意义。
3种非简单类型如下。
- string,它是一个Unicode字符数组。
- object,它是所有其他类型的基类。
- dynamic,使用动态语言编写的程序集时使用。
预定义简单类型
名称 含义 范围 .NET框架类型 默认值 sbyte 8位有符号整数 -128~127 System.SByte 0 byte 8位无符号整数 0~255 System.Byte 0 short 16位有符号整数 -32 768~32 767 System.Int16 0 ushort 16位无符号整数 0~65 535 System.UInt16 0 int 32位有符号整数 -2 147 483 648~2 147 483 647 System.Int32 0 uint 32位无符号整数 0~4 294 967 295 System.UInt32 0 long 64位有符号整数 -9 223 372 036 854 775 808
~9 223 372 036 854 775 807System.Int64 0 ulong 64位无符号整数 0~18 446 744 073 709 551 615 System.UInt64 0 float 单精度浮点数 1.5×10-45~3.4×1038 System.Single 0.0f double 双精度浮点数 5×10-324~1.7×10308 System.Double 0.0d bool 布尔型 true false System.Boolean false char Unicode 字符串 U+0000~U+ffff System.Char \x0000 decimal 小数类型的有效数字精度为28位 ±1.0×1028~±7.9×1028 System.Decimal 0m 非简单预定义类型稍微复杂一些。表3-2所示为非简单预定义类型。 预定义非简单类型
名称 含义 .NET框架类型 object 所有其他类型的基类,包括简单类型 System.Object string 0个或多个Unicode字符所组成的序列 System.String dynamic 在使用动态语言编写的程序集时使用 无相应的.NET类型 用户定义类型
除了C#提供的16种预定义类型,还可以创建自己的用户定义类型。有6种类型可以由用户自己创建,它们是:
- 类类型(class);
- 结构类型(struct);
- 数组类型(array);
- 枚举类型(enum);
- 委托类型(delegate);
- 接口类型(interface)。
类型通过类型声明创建,类型声明包含以下信息:
- 要创建的类型的种类;
- 新类型的名称;
- 对类型中每个成员的声明(名称和规格)。array和delegate类型除外,它们不含有命名成员。
一旦声明了类型,就可以创建和使用这种类型的对象,就像它们是预定义类型一样。
堆和栈
运行中的程序使用两个内存区域来存储数据:栈和堆。
栈
栈是一个内存数组,是一个LIFO(Last-In First-Out,后进先出)的数据结构。栈存储几种类型的数据:
- 某些类型变量的值;
- 程序当前的执行环境;
- 传递给方法的参数。
系统管理所有的栈操作。作为程序员,你不需要显式地对它做任何事情。但了解栈的基本功能可以更好地了解程序在运行时正在做什么,并能更好地了解C#文档和著作。
栈的特征
栈有如下几个普遍特征。
- 数据只能从栈的顶端插入和删除。
- 把数据放到栈顶称为入栈(push)。
- 从栈顶删除数据称为出栈(pop)。
堆
堆是一块内存区域,在堆里可以分配大块的内存用于存储某类型的数据对象。与栈不同,堆里的内存能够以任意顺序存入和移除。
展示了一个在堆里放了4项数据的程序
虽然程序可以在堆里保存数据,但并不能显式地删除它们。CLR的自动GC(Garbage Collector,垃圾收集器)在判断出程序的代码将不会再访问某数据项时,自动清除无主的堆对象。我们因此可以不再操心这项使用其他编程语言时非常容易出错的工作了。
阐明垃圾收集过程
值类型和引用类型
数据项的类型定义了存储数据需要的内存大小及组成该类型的数据成员。类型还决定了对象在内存中的存储位置——栈或堆。
类型被分为两种:值类型和引用类型,这两种类型的对象在内存中的存储方式不同。
- 值类型只需要一段单独的内存,用于存储实际的数据。
- 引用类型需要两段内存。
- 第一段存储实际的数据,它总是位于堆中。
- 第二段是一个引用,指向数据在堆中的存放位置。
每种类型的单个数据项是如何存储的?对于值类型,数据存放在栈里。对于引用类型,实际数据存放在堆里而引用存放在栈里。
图3-9 非成员数据的存储
存储引用类型对象的成员
图3-9阐明了当数据不是另一个对象的成员时如何存储。如果它是另一个对象的成员,那么它的存储会有些不同。
- 引用类型对象的数据部分始终存放在堆里,如图3-9所示。
- 值类型对象,或引用类型数据的引用部分可以存放在堆里,也可以存放在栈里,这依赖于实际环境。
例如,假设有一个引用类型的实例,名称为MyType,它有两个成员:一个值类型成员和一个引用类型成员。它将如何存储呢?是否是值类型的成员存储在栈里,而引用类型的成员如图3-9所示的那样在栈和堆之间分成两半呢?答案是否定的。
请记住,对于一个引用类型,其实例的数据部分始终存放在堆里。既然两个成员都是对象数据的一部分,那么它们都会被存放在堆里,无论它们是值类型还是引用类型。图3-10阐明了MyType的情形。
- 尽管成员A是值类型,但它也是MyType实例数据的一部分,因此和对象的数据一起被存放在堆里。
- 成员B是引用类型,所以它的数据部分会始终存放在堆里,正如图中“数据”框所示。不同的是,它的引用部分也被存放在堆里,封装在MyType对象的数据部分中。
图3-10 引用类型成员数据的存储
说明 对于引用类型的任何对象,它所有的数据成员都存放在堆里,无论它们是值类型还是引用类型。
C#类型的分类
表3-3列出了C#中可以使用的所有类型以及它们的类别:值类型或引用类型。每种引用类型都将在后面的内容中阐述。
表3-3 C#中的值类型和引用类型
值类型 引用类型 预定义类型 sbyte byte float
short ushort double
int uint char
long ulong decimal
boolobject
string
dynamic用户定义类型 struct
enumclass
interface
delegate
array变量
一种多用途的编程语言必须允许程序存取数据,而这正是通过变量实现的。
- 变量是一个名称,表示程序执行时存储在内存中的数据。
- C#提供了4种变量,每一种都将详细讨论。表3-4列出了变量的种类。
表3-4 4种变量
名称 描述 局部变量 在方法的作用域保存临时数据,不是类型的成员 字段 保存和类型或类型实例相关的数据,是类型的成员 参数 用于从一个方法到另一个方法传递数据的临时变量,不是类型的成员 数组元素 (通常是)同类数据项构成的有序集合的一个成员,可以为本地变量,也可以为类型的成员 4. 类
一个 C#类可以有任意数目的数据成员和函数成员。成员可以是9种成员类型的任意组合。
表4-1 类成员的类型
数据成员存储数据 函数成员执行代码 字段、常量 方法、属性、构造函数、析构函数、运算符、索引器、事件 类成员
字段和方法是最重要的类成员类型。字段是数据成员,方法是函数成员。
字段
字段是隶属于类的变量。
- 它可以是任何类型,无论是预定义类型还是用户定义类型。
- 和所有变量一样,字段用来保存数据,并具有如下特征:
- 可以被写入
- 可以被读取
方法
方法是具有名称的可执行代码块,可以从程序的很多不同地方执行,甚至从其他程序中执行。
声明方法的最简语法包括以下组成部分:
- 返回类型
- 名称
- 参数列表
- 方法体
创建变量和类的实例
类的声明只是用于创建类的实例的蓝图。一旦类被声明,就可以创建类的实例。
- 类是引用类型,它们要为数据引用和实际数据都申请内存。
- 数据的引用保存在一个类类型的变量中。所以,要创建类的实例,需要从声明一个类类型的变量开始。如果变量没有被初始化,他的值是未定义的。
访问修饰符
访问修饰符是成员声明的可选部分,指明程序的其他部分如何访问成员。
- 私有的:private
- 公有的:public
- 受保护的:protected
- 内部的:internal
- 受保护内部的:protected internal
私有访问和公有访问
私有成员只能从声明他的类的内部访问,其他的类看不见或无法访问:
- 私有访问是默认的访问级别,如果一个成员在声明时不带访问修饰符,那它就是私有成员。
- 还可以显式的声明私有成员。
例如,下面的两个声明都指定了 private int 成员:
int MyInt1; //隐式声明为私有private int MyInt2; //显式声明为私有
实例的公有成员可以被程序中的其他对象访问。必须使用 public 访问修饰符指定公有访问。
5. 方法
方法主要有两个部分:方法头和方法体。
- 方法头指定方法的特征,包括:
- 方法是否返回数据,如果返回,返回类型。
- 方法的名称。
- 哪种类型的数据可以传递给方法或从方法返回,以及应如何处理这些数据。
- 方法体包含可以执行的语句序列。执行过程从方法体的第一条语句开始,一直到整个方法结束。
int MyMethod(int par1, string par2) //方法头{Console.WriteLine("First"); //方法体}局部变量
局部变量和字段一样也保存数据。字段通常保存和对象状态有关的数据,而创建局部变量经常是用于保存局部的或临时的计算数据。
字段实例 局部变量 生存期 从实例被创建时开始,直到实例不再被访问时结束 从它在块中被声明的那一刻开始,在块完成执行时结束 隐式初始化 初始化成该类型的默认值 没有隐式初始化。如果变量在使用之前没有被赋值,编译器就会产生一条错误信息。 存储区域 由于实例字段是类的成员,所以所有字段都存储在堆里,无论他们是值类型还是引用类型。 引用类型:引用存储在栈里,数据存储在堆里;
值类型:存储在栈里var 关键字
使用var关键字来声明局部变量,其结果依然是一个类型确定的局部变量。唯一的区别是,不再明确写出类型信息,而是由编译器根据变量的赋值在编译时推断出来。
使用 var 关键字有一些重要条件:
- 只能用于局部变量,不能用于字段;
- 只能在变量声明中包含初始化时使用;
- 一旦编译器推断出变量的类型,它就是固定且不能更改的。
局部常量
局部常量很像局部变量,只是一旦被初始化,它的值就不能改变了。如同局部变量,局部常量必须声明在块的内部。
常量的两个最重要的特征如下:
- 在声明时必须初始化。
- 在声明后不能改变。
关键字:const
const TYPE Identifier = Value;
局部函数
从 C#7.0 开始,你可以在一个方法中声明另一个单独的方法。
参数
引用类型的实参,实参和形参都引用堆中的同一个对象,执行方法结束后,他的值被方法的行为改变了。
值类型的实参,系统在栈上为形参分配内存,执行方法结束后,形参从栈中弹出,他的值不受方法行为的影响。
引用参数
- 使用引用参数时,必须在方法的声明和调用中都使用 ref 修饰符。
- 实参必须是变量,在用作实参前必须被赋值。如果是引用类型变量,可以赋值为一个引用或 null。
void MyMethod( ref int val) //方法声明{...}int y = 1; //实参变量MyMethod( ref y) //方法调用MyMethod( ref 3+5 ) //出错了,没有使用变量引用参数具有的特征:
- 不会再栈上为形参分配内存。
- 形参的参数名将作为实参变量的别名,指向相同的内存位置。
由于形参名和实参名指向相同的内存位置,所以再方法的执行过程中对形参做的任何改变在方法完成后依然可见。
引用类型作为值参数和引用参数
- 将引用类型对象作为值参数传递:如果在方法内创建一个新对象并赋值给形参,将切断形参与实参之间的关联,并且在方法调用结束后,新对象也将不复存在。
- 将引用类型对象作为引用参数传递:如果在方法内创建一个新对象并赋值给形参,在方法结束后该对象依然存在,并且是实参所引用的值。
第一种情况 将引用类型对象作为值参数传递
class Program { static void Main(string[] args) { MyClass a1 = new MyClass(); Console.WriteLine(a1.Val); Change(a1); Console.WriteLine(a1.Val); } static void Change(MyClass f1) { f1.Val = 50; Console.WriteLine(f1.Val); f1 = new MyClass(); Console.WriteLine(f1.Val); } } class MyClass { public int Val = 20; }输出结果:
20502050
下图阐明了关于上述代码的以下几点:
- 在方法开始时,实参和形参都指向堆中相同的对象。
- 在为对象的成员赋值之后,它们仍指向堆中相同的对象。
- 当方法分配新的对象并赋值给形参时,(方法外部的)实参仍指向原始对象,而形参指向的是新对象。
- 在方法调用之后,实参指向原始对象,形参和新对象都会消失。
class Program{static void Main(string[] args){MyClass a1 = new MyClass();Console.WriteLine(a1.Val);Change(ref a1);Console.WriteLine(a1.Val);}static void Change(ref MyClass f1){//设置对象成员f1.Val = 50;Console.WriteLine(f1.Val);//创建新对象并赋值给形参f1 = new MyClass();Console.WriteLine(f1.Val);}}class MyClass{public int Val = 20;}输出结果:
20502020
下图阐明了上述代码的以下几点:
- 在方法调用时,形参和实参都指向堆中相同的对象
- 对成员值的修改会同时影响到形参和实参。
- 当方法创建新的对象并赋值给形参时,形参和实参的引用都指向该新对象。
- 在方法结東后,实参指向在方法内创建的新对象。
输出参数
输出参数用于从方法体内把数据传出到调用代码,他们的行为与引用参数类似。
输出参数有以下要求:
- 必须在声明和调用中都是用修饰符。输出参数的修饰符是 out 。
- 实参必须是变量,而不能是其他类型的表达式。
- 在方法内部,给输出参数赋值之后才能读取它。这意味着参数的初始值是无关的,而且没有必要在方法调用之前为实参赋值。
- 在方法内部,在方法返回之前,代码中每条可能的路径都必须为所有输出参数赋值。
与引用参数类似,输出参数的形参充当实参的别名。形参和实参都是同一块内存位置的名称。在方法内对形参做的任何改变在方法执行完成之后(通过实参变量)都是可见的。
如果方法中有任何执行路径试图在输出参数被方法赋值之前读取它,编译器就会产生一条错误信息。
public void Add2 ( out int outValue ){int var1 = outValue + 2; //错误!在方法赋值之前,无法读取输出变量}例如,下面的代码在此展示了方法MyMethod,但这次使用输出参数。
class Program { static void Main(string[] args) { MyClass a1 = null; int a2; MyMethod(out a1, out a2);//调用方法 } static void MyMethod(out MyClass f1,out int f2) { f1 = new MyClass();//创建一个类变量 f1.Val = 25;//赋值类的字段 f2 = 15;//赋值int参数 } } class MyClass { public int Val = 20;//初始化字段为20 }下图阐述了在方法执行的不同阶段中实参和形参的值:
- 在方法调用之前,将要被用作实参的变量a1和a2已经在栈里了。
- 在方法的开始,形参的名称设置为实参的别名。你可以认为变量a1和f1指向的是相同的内存位置,也可以认为a2和f2指向的是相同的内存位置。a1和a2不在作用域之内,所以不能在 MyMethod中访问。
- 在方法内部,代码创建了一个MyClass类型的对象并把它赋值给f1,然后赋一个值给f1的字段,也赋一个值给f2。对f1和f2的赋值都是必需的,因为它们是输出参数。
- 方法执行之后,形参的名称已经失效,但是引用类型的a1和值类型的a2的值都被方法内的行为改变了。
对于输出参数,形参就好像是实参的别名一样,但是还有一个需求,那就是它必须在方法内进行赋值
从 C#7.0 开始,你不在需要预先成名一个变量来用作 out 参数了。你可以在调用方法时在参数列表中添加一个变量类型,它将作为变量声明。
- 消除显式的变量声明;
- 直接在方法调用时加入变量类型声明;
例如:
static void Main(){MyMethod(out MyClass a1, out int a2); //调用方法}虽然 a1 和 a2 只在方法调用语句中进行了声明,但它们也可以在方法调用完后继续使用。
参数数组
参数数组允许特定类型的零个或多个实参对应一个特定的形参。
参数数组的重点如下:
- 在一个参数列表中只能有一个参数数组;
- 如果有,它必须是列表中的最后一个;
- 由参数数组表示的所有参数必须是同一类型;
声明一个参数数组时必须做的事如下:
- 在数据类型前使用 params 修饰符。
- 在数据类型后放置一组空的方括号。
例:
void ListInts( params int[] inVals){ }- 数组是个引用类型,因此它的所有数据项都保存在堆中。
方法调用
//方式一:1. ListInt(1,2,3); //3个int //方式二:2. int[] intArray = {1,2,3}; ListInt(intArray);//一个数组变量调用时不允许有 params 修饰符。
当数组在堆中被创建时,实参的值被复制到数组中,这样它们就像值参数。
- 如果数组参数是值类型,那么值被复制,实参在方法内部不受影响。
- 如果数组参数是引用类型,那么引用被复制,实参引用的对象在方法内部会受到影响。
在方法调用之前创建并组装一个数组,把单一的数组变量作为实参传递。这种情况下编译器使用你的数组而不是重新创建一个。
参数类型总结
参数类型 修饰符 是否在声明时使用 是否在调用时使用 执行 值 无 系统把实参的值复制到形参 引用 ref 是 是 形参是实参的别名 输出 out 是 是 仅包含一个返回的值,形参是实参的别名 数组 params 是 否 允许传递可变数目的实参到方法 ref 局部变量和 ref 返回
ref 局部变量功能的重要事项:
- 你可以使用这个功能创建一个变量的别名,即使引用的对象是值类型。
- 对任意一个变量的赋值都会反映到另一个变量上,因为他们引用的是相同的对象,即使是值类型。
例:
//创建别名的语法需要使用关键字 ref 两次,一次是在别名声明的类型的前面,另一次是在赋值运算符的右边,“被别名”的变量的前面ref int y = ref x;
ref 返回功能提供了一种方法返回变量引用而不是变量值的方法。这里也使用了 ref 关键字两次:
- 一次是在方法的返回类型声明之前
- 另一次是在 return 关键字之后,被返回对象的变量名之前
class Simple{ private int Score = 5; // ref 返回方法的关键字 public ref int RefToValue() { //ref 返回方法的关键字 return ref Score; } public void Display() { Console.WriteLine($"{Score}"); }}class Program{ static void Main(string[] args) { Simple s = new Simple(); s.Display();//Value inside class object: 5 //此时返回的是成员在内存上的引用地址, 而不单纯是值 ref int v1OutSide = ref s.RefToValue(); //在调用域外面修改值, 直接修改了成员的值 v1OutSide = 10; s.Dsiplay();//Value inside class object: 10 }}Math库中的Max方法的变形, 提供两个数字类型的变量, Math.Max能够返回两个值中比较大的那个, 但是假设你想返回的是包含较大值的变量的引用, 可以用ref返回
class Program{ public static ref int Max(ref int p1, ref int p2) { if (p1 > p2) return ref p1; else return ref p2; }static void Main(string[] args){int v1 = 10;int v2 = 20;Console.WriteLine("start");Console.WriteLine($"v1 = {v1}, v2 = {v2}");// 10, 20ref int max = ref Max(ref v1, ref v2);Console.WriteLine("after method");Console.WriteLine($"max = {max}");//20max++;Console.WriteLine("after increment");Console.WriteLine($"v1 = {v1}, v2 = {v2}");// 10, 21}}注意:
- ref return 表达式不能返回如下内容:
- 空值
- 常量
- 枚举成员
- 类或者结构体的属性
- 指向只读位置的指针
- ref return 表达式只能指向原先就在调用域内的位置,或者字段。所以,他不能指向方法的局部变量。
- ref 局部变量只能被赋值一次,也就是说,一旦初始化,他就不能指向不同的存储位置了。
- 即使将一个方法声明为 ref 返回方法,如果在调用该方法时省略了 ref 关键字,则返回的将是值,而不是指向值的内存位置的指针。
- 如果将 ref 局部连梁作为常规的实际参数传递给其他方法,则该方法仅获取该变量的一个副本。尽管 ref 局部变量包含指向存储位置的指针,但是当以这种方式使用时,他会传递值而不是引用。
方法重载
一个类中可以有多个同名方法,这叫作方法重载(method overloading)。使用相同名称的每个方法必须有一个和其他方法不同的签名(signature)。
- 方法的签名由下列信息组成,他们在方法声明的方法头中:
- 方法的名称;
- 参数的数目;
- 参数的数据类型和顺序;
- 参数修饰符;
- 返回类型和形参的名称不是签名的一部分。
命名参数
C#允许我们使用命名参数(named parameter)。只要显式指定参数的名字,就可以以任意顺序在方法调用中列出实参。
- 方法的声明没有什么不一样。形参已经有名字了。
- 不过在调用方法的时候,形参的名字后面跟着冒号和实际的参数值或表达式。
例:
public static int Calc(int a,int b,int c){...};static Main(){Calc(c:2,a:4,b:3);}可选参数
所谓可选参数就是可以在调用方法的时候包含这个参数,也可以省略它。
为了表明某个参数是可选的,你需要在方法声明中为该参数提供默认值。
例:
// 参数b为可选参数,默认值为3public int Calc(int a, int b = 3){...}可选参数声明的注意事项:
- 不是所有的参数类型都可以作为可选参数。
- 只要值类型的默认值在编译的时候可以确定,就可以使用值类型作为可选参数。
- 只有在默认值是 null 的时候,引用类型才可以用作可选参数。
- 所有必填参数(required parameter) 必须在可选参数声明之前声明。如果有 params 参数,必须在所有可选参数之后声明。
语法顺序:必填参数-可选参数-params参数
栈帧
在调用方法的时候,内存从栈的顶部开始分配,保存和方法关联的一些数据项。这块内存叫作方法的栈帧(stack frame)。
栈帧包含的内存保存如下内容:
- 返回地址,也就是在方法退出的时候继续执行的位置。
- 分配内存的参数,也就是方法的值参数,还可能是参数数组(如果有的话)。
- 和方法调用相关的其他管理数据项。
在方法调用时,整个栈帧都会压入栈。
在方法退出的时候,整个栈帧都会从栈上弹出。弹出栈帧有的时候也叫作栈展开(unwind)。
递归
调用方法自身的机制,每一次方法调用把新的栈帧压入栈顶。
6.深入理解类
成员修饰符的顺序
类成员声明语句由下列部分组成:核心声明、一组可选的修饰符和一组可选的特性(attribute)。
[特性] [修饰符] 核心声明
方括号表示方括号内的成分是可选的。
- 修饰符
- 如果由修饰符,必须放在核心声明之前。
- 如果有多个修饰符,可以任意顺序排列。
- 特性
- 如果有特性,必须放在修饰符和核心声明之前。
- 如果有多个特性,可以任意顺序排列。
静态字段
除了实例字段,类还可以拥有静态字段。
- 静态字段被类的所有实例共享,所有实例都访问同一内存位置。因此,如果该内存位置的值被一个实例改变了,这种改变对所有的实例都可见。
- 可以使用 static 修饰符将字段声明为静态
可以通过类名.静态成员名的方式从类的外部访问静态成员。
静态成员的生存期与实例成员不同,即使类没有实例,也存在静态成员,并且可以访问。
除了静态字段,还有静态函数成员。
- 如同静态字段,静态函数成员独立于任何类实例,没有类的实例也能调用。
- 静态函数成员不能访问实例成员,但能访问其他静态成员。
可以声明为static的类成员类型包括:
- 字段
- 类型
- 方法
- 属性
- 构造函数
- 运算符
- 事件
不可以声明为static的类成员类型包括:
- 常量
- 索引器
成员常量
成员常量和局部常量类似,只是它们被声明在类声明中而不是方法内。
需要在声明时赋值,之后不能改变。
例:
class MyClass{const int IntVal = 100; //成员常量public void MyMethod(){const int PartIntVal = 10; //局部常量}}成员常量对类的每个实例都是“可见的”,而且即使没有类的实例也可以使用。常量 const 没有自己的存储位置,而是在编译时被编译器替换。这种方式类似是C/C++中的#define值。
属性
属性是代表类实例或类中的数据项的成员。使用属性就像写入或读取一个字段,语法相同。
与字段类似,属性有以下特征:
- 它是命名的类成员。
- 他又类型。
- 它可以被赋值和读取。
与字段的不同,属性是一个函数成员。
- 它不一定为数据存储分配内存
- 他执行代码。
属性是一组(两个)匹配的、命名的、称为访问器的方法。
- set 访问器为属性赋值
- get 访问器从属性获取值
访问器
set 访问器总是:
- 拥有一个单独的、隐式的值参,名称为value,与属性的类型相同。
- 拥有一个返回类型 void
get 访问器总是:
- 没有参数
- 拥有一个与属性类型相同的返回类型
C#7.0 为属性的 getter/setter 引入了另一种语法,这种语法使用表达函数体(lambda表达式)。
int MyValue{set => value > 100 ? 100:value;get => theRealValue;}属性和字段的命名约定
第一种约定:两个名称使用相同的内容,但字段使用 Camel 大小写,属性使用 Pascal 大小写。
private int firstField; //Camelpublic int FirstField {get;set;}//Pascal第二种约定:属性使用 Pascal 大小写,字段使用相同标识符的Camel 大小写版本,并以下划线开始。
private int _secondField; //下划线开始 Camelpublic int SecondField {get;set;}属性与公有字段
属性比公有字段更好,理由如下:
- 属性是函数成员而不是数据成员,允许你处理输入和输出,而公有字段不行。
- 属性可以只读或只写,而字段不行。
- 编译后的变量和编译后的属性语义不同。
自动实现属性
因为属性经常被关联到后备字段,所以 C# 提供了自动实现属性这个特性,允许只声明属性而不生命后备字段。
自动实现属性的要点:
- 不声明后备字段:编译器根据属性的类型分配内存。
- 不能提供访问器的方法体:它们必须被简单地声明为分号。get 担当简单的内存读,set 担当简单的写。
class C1{public int MyValue{get;set;}}静态属性
属性也可以声明为 static。静态属性的访问器和所有静态成员一样,具有以下特点:
- 不能访问类的实例成员,但能被实例成员访问。
- 不管类是否有实例,它们都是存在的。
- 在类的内部,可以仅使用名称来引用静态属性。
- 在类的外部,可以通过类名或者使用 using static 结构来引用属性。
实例构造函数
- 构造函数用于初始化类实例的状态。
- 如果希望能从类的外部创建类的实例,需要将构造函数声明为 public。
- 构造函数的名称和类名相同。
- 构造函数不能有返回值。
- 构造函数可以带参数。
- 构造函数可以被重载。
例:
class MyClass{//构造函数public MyClass(){...}}默认构造函数
如果在类的声明中没有显式的提供实例构造函数,那么编译器会提供一个隐式的默认构造函数,它没有参数,方法体为空。
如果你声明了任何构造函数,那么编译器将不会为该类定义默认构造函数。所以此时如果使用无参的构造函数创建新实例,编译器会报错。
静态构造函数
构造函数也可以声明为 static。实力构造函数初始化类的每个新实例,而 static 构造函数初始化类级别的项。通常静态构造函数初始化类的静态字段。
- 初始化类级别的项。
- 在引用任何静态成员之前。
- 在创建类的任何实例之前。
- 静态构造函数在以下方面与实例构造函数类似
- 静态构造函数的名称必须和类名相同。
- 不能有返回值
- 静态构造函数在以下方面和实例构造函数不同。
- 静态构造函数声明中使用 static 关键字。
- 类只能有一个静态构造函数,而且不能带参数。
- 静态构造函数不能有访问修饰符。
例:
class C1{private static Random RandomKey; //私有静态字段//静态构造函数static C1(){RandomKey = new Random(); //执行所有静态初始化}public int GetRandomNumber(){return RandomKey.Next();}}- 类既可以有静态构造也可以有实例构造。
- 如同静态方法,静态构造函数不能访问所在类的实例成员,因此也不能使用 this 访问器。
- 不能从程序中显式调用静态构造函数,系统会自动调用它们:
- 初始化类级别的项。
- 修饰符
- 不是所有的参数类型都可以作为可选参数。
- 方法的签名由下列信息组成,他们在方法声明的方法头中:
- ref return 表达式不能返回如下内容:
- 数组是个引用类型,因此它的所有数据项都保存在堆中。
- 方法头指定方法的特征,包括:
- 11种数值类型。
-
-