c mvc 大型网站开发,快速搭建论坛,视频直播网站开发流程,网站免费软件推荐文章目录 1. C#和.NET框架.NET框架的组成.NET框架的特点CLRCLICLI的重要组成部分各种缩写 2. C#编程概括标识符命名规则#xff1a; 多重标记和值格式化数字字符串对齐说明符格式字段标准数字格式说明符标准数字格式说明符 表 3. 类型、存储和变量数据成员和函数成员预定义类型… 文章目录 1. C#和.NET框架.NET框架的组成.NET框架的特点CLRCLICLI的重要组成部分各种缩写 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 XMLXML 基础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框架由三部分组成严格地说CLRCommon Language Runtime公共语言运行库和FCL框架类库两部分组成不包括工具。FCL是BCL的超集还包括Windows Forms、ASP.NET、LINQ以及更多命名空间
.NET框架的特点
面向对象的开发环境自动垃圾收集CLR有一项服务称为GCGarbage Collector垃圾收集器它能为你自动管理内存。互操作性不需要COM简化的部署类型安全性基类库
CLR
.NET框架的核心组件是CLR它在操作系统的顶层负责管理程序的执行。 CLR还提供下列服务
自动垃圾收集安全和认证通过访问BCL得到广泛的编程功能包括如Web服务和数据服务之类的功能。
CLI
CLICommon Language Infrastructure公共语言基础结构就是一组标准它把所有.NET框架的组件连结成一个内聚的、一致的系统。它展示了系统的概念和架构并详细说明了所有软件都必须坚持的规则和约定。
CLI的重要组成部分 公共类型系统 CTSCommon Type System公共类型系统定义了那些在托管代码中一定会使用的类型的特征。CTS的一些重要方面如下。 CTS定义了一组丰富的内置类型以及每种类型固有的、独有的特性。.NET兼容编程语言提供的类型通常映射到CTS中已定义的内置类型集的某一个特殊子集。CTS最重要的特征之一是所有类型都继承自公共的基类——object。使用CTS可以确保系统类型和用户定义类型能够被任何.NET兼容的语言所使用。 公共语言规范 CLSCommon Language Specification公共语言规范详细说明了一个.NET兼容编程语言的规则、属性和行为其主题包括数据类型、类结构和参数传递。
各种缩写 2. C#编程概括
标识符
标识符是一种字符串用来命名变量、方法、参数和许多后面将要阐述的其他程序结构。
命名规则
字母和下划线a-z、A-Z 和 _ 可以用在任何位置。数字不能放在首位但可以放在其他的任何位置。字符只能放在标识符的首位。虽然允许使用但不推荐
多重标记和值
在 C#中可以使用任意数量的替代标记和任意数量的值。
值可以以任何顺序使用。只可以在格式字符串中替换任意次。 例如
Console.WriteLine(Three integers are {1},{0} and {1}., 3, 6);控制台输出结果
Three integers are 63 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个内置字符格式之一。字符可以是大写或小写形式。大小写对于某些说明符来说比较重要而对于另外一些说明符来说则不重要。精度说明符是可选的由12位数字组成。它的实际意义取决于格式说明符。 图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 -- General
12.345678 -- Default, same as General
12.3457 -- Fixed Point, 4 dec places
$12.35 -- Currency
1.235E001 -- Sci. Notation, 3 dec places
123adf -- 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 区分大小写十六进制数字的字符串。十六进制数字AF会匹配说明符的大小写形式 精度说明符输出字符串中的最少位数。如果实际数的位数更少则在左边以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.2346e001
3. 类型、存储和变量
数据成员和函数成员
像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框架类型默认值sbyte8位有符号整数-128~127System.SByte0byte8位无符号整数0~255System.Byte0short16位有符号整数-32 768~32 767System.Int160ushort16位无符号整数0~65 535System.UInt160int32位有符号整数-2 147 483 648~2 147 483 647System.Int320uint32位无符号整数0~4 294 967 295System.UInt320long64位有符号整数-9 223 372 036 854 775 808 ~9 223 372 036 854 775 807System.Int640ulong64位无符号整数0~18 446 744 073 709 551 615System.UInt640float单精度浮点数1.5×10-45~3.4×1038System.Single0.0fdouble双精度浮点数5×10-324~1.7×10308System.Double0.0dbool布尔型true falseSystem.BooleanfalsecharUnicode 字符串U0000~UffffSystem.Char\x0000decimal小数类型的有效数字精度为28位±1.0×1028~±7.9×1028System.Decimal0m非简单预定义类型稍微复杂一些。表3-2所示为非简单预定义类型。
预定义非简单类型
名称含义.NET框架类型object所有其他类型的基类包括简单类型System.Objectstring0个或多个Unicode字符所组成的序列System.Stringdynamic在使用动态语言编写的程序集时使用无相应的.NET类型
用户定义类型
除了C#提供的16种预定义类型还可以创建自己的用户定义类型。有6种类型可以由用户自己创建它们是
类类型class结构类型struct数组类型array枚举类型enum委托类型delegate接口类型interface。
类型通过类型声明创建类型声明包含以下信息
要创建的类型的种类新类型的名称对类型中每个成员的声明名称和规格。array和delegate类型除外它们不含有命名成员。
一旦声明了类型就可以创建和使用这种类型的对象就像它们是预定义类型一样。
堆和栈
运行中的程序使用两个内存区域来存储数据栈和堆。
栈
栈是一个内存数组是一个LIFOLast-In First-Out后进先出的数据结构。栈存储几种类型的数据
某些类型变量的值程序当前的执行环境传递给方法的参数。
系统管理所有的栈操作。作为程序员你不需要显式地对它做任何事情。但了解栈的基本功能可以更好地了解程序在运行时正在做什么并能更好地了解C#文档和著作。
栈的特征
栈有如下几个普遍特征。
数据只能从栈的顶端插入和删除。把数据放到栈顶称为入栈push。从栈顶删除数据称为出栈pop。 堆
堆是一块内存区域在堆里可以分配大块的内存用于存储某类型的数据对象。与栈不同堆里的内存能够以任意顺序存入和移除。
展示了一个在堆里放了4项数据的程序
虽然程序可以在堆里保存数据但并不能显式地删除它们。CLR的自动GCGarbage 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 35 ) //出错了没有使用变量引用参数具有的特征
不会再栈上为形参分配内存。形参的参数名将作为实参变量的别名指向相同的内存位置。 由于形参名和实参名指向相同的内存位置所以再方法的执行过程中对形参做的任何改变在方法完成后依然可见。 引用类型作为值参数和引用参数
将引用类型对象作为值参数传递如果在方法内创建一个新对象并赋值给形参将切断形参与实参之间的关联并且在方法调用结束后新对象也将不复存在。将引用类型对象作为引用参数传递如果在方法内创建一个新对象并赋值给形参在方法结束后该对象依然存在并且是实参所引用的值。
第一种情况 将引用类型对象作为值参数传递
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;}输出结果
20
50
20
50下图阐明了关于上述代码的以下几点
在方法开始时实参和形参都指向堆中相同的对象。在为对象的成员赋值之后它们仍指向堆中相同的对象。当方法分配新的对象并赋值给形参时方法外部的实参仍指向原始对象而形参指向的是新对象。在方法调用之后实参指向原始对象形参和新对象都会消失。
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;
}输出结果
20
50
20
20下图阐明了上述代码的以下几点
在方法调用时形参和实参都指向堆中相同的对象对成员值的修改会同时影响到形参和实参。当方法创建新的对象并赋值给形参时形参和实参的引用都指向该新对象。在方法结東后实参指向在方法内创建的新对象。 输出参数
输出参数用于从方法体内把数据传出到调用代码他们的行为与引用参数类似。 输出参数有以下要求
必须在声明和调用中都是用修饰符。输出参数的修饰符是 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;elsereturn 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为可选参数默认值为3
public 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; //Camel
public int FirstField {get;set;}//Pascal第二种约定属性使用 Pascal 大小写字段使用相同标识符的Camel 大小写版本并以下划线开始。
private int _secondField; //下划线开始 Camel
public 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 访问器。不能从程序中显式调用静态构造函数系统会自动调用它们 在类的任何实例被创建之前。在类的任何静态成员引用之前。
readonly 修饰符
字段可以用 readonly 修饰符声明。起作用类似于将字段声明为 const一旦值被设定就不能改变。
const 字段只能在字段的声明语句中初始化而 readonly 字段可以在下列任意位置设置它的值。 字段声明语句类似于 const。类的任何构造函数。如果是 static 字段初始化必须在静态构造函数中完成。 const 字段的值必须可在编译时决定而 readonly 字段的值可以在运行时决定。const 的行为总是静态的而对于 readonly 字段以下两点是正确的。 它可以是实例字段也可以是静态字段。他在内存中有存储位置。
this 关键字
this 关键字在类中使用是对当前实例的引用。 他只能被用于在下列类成员的代码块中。
实例构造函数实例方法属性和索引器的实例访问器。
this 的作用
用于区分类的成员和局部变量或参数作为调用方法的实参
索引器
索引器是一组 get 和 set 访问器与属性类似。索引器使用索引运算符它有一对方括号和中间的索引组成。
索引器和属性
相似点
和属性一样索引器不用分配内存来存储。索引器和属性都主要被用来访问其他数据成员它们与这些成员关联并为它们提供获取和设置访问。 属性通常表示单个数据成员。索引器通常表示多个数据成员。 索引器的注意点 和属性一样索引器可以只有一个访问器也可以两个多有。索引器总是实例成员因此不能被声明为 static。和属性一样实现get 和 set 访问器的代码不一定要关联到某个字段和属性。这段代码可以做任何事情也可以什么都不做只要 get 访问器返回某个指定类型的值即可。
声明索引器
声明索引器的语法如下。
索引器没有名称在名称的位置是 this 关键字。参数列表在方括号中间。参数列表中必须至少声明一个参数。
ReturnType this [Type param1,...]
{get{...}set{...}
}示例
public class Employee
{ public string LastName; public string FirstName; public string CityOfBirth; public string this[int index] { set { switch (index) { case 0: LastName value; break; case 1: FirstName value; break; case 2: CityOfBirth value; break; default: throw new ArgumentOutOfRangeException(index); } } get { switch (index) { case 0: return LastName; case 1: return FirstName; case 2: return CityOfBirth; default: throw new ArgumentOutOfRangeException(index); } } }
}索引器重载
只要索引器的参数列表不同类就可以有任意多个索引器。 例
class MyClass
{public string this[int index]{get{ }set{ }}public string this[int index1,int index2]{get{ }set{ }}public int this[float index1]{get{ }set{ }}
}访问器的访问修饰符
默认情况下成员的两个访问器的访问级别和成员自身相同如果一个属性的访问级别是public 那么它的两个访问器的访问级别也是如此。
访问器的访问修饰符有几个限制
仅当成员属性或索引器既有get访问器也有 set 访问器时其访问器才能有访问修饰符。虽然两个访问器都必须出现但它们中只能有一个有访问修饰符。访问器的访问修饰符的限制必须比成员的访问级别更严格。
分部类和分部类型
类的声明可以分割成几个分部类的声明。
每个分部类的声明都含有一些类成员的声明。类的分部类声明可以在同一个文件中也可以在不同的文件中。 每个分部类声明必须标注为 partial class而不是单独的关键字 class。 类型修饰符 partial 不是关键字所以在其他上下文中可以在程序中把它用作标识符。但直接用在关键字 class、struct 或 interface 之前时他表示分部类型。 分部方法
分部方法是声明在分部类中不同部分的方法。分部方法的不同部分也可以声明在分部类的不同部分中也可以声明在同一个部分中。 分部方法的两个部分如下
定义分部方法声明 给出签名和返回类型。声明的实现部分只是一个分号。 实现分部方法声明 给出签名和返回类型以普通的语句块形式实现。 分部方法的注意点
定义声明和实现声明的签名和返回类型必须匹配。签名和返回类型有如下特征 返回类型必须是void签名不能包括访问修饰符这使分部方法是隐式私有的。参数列表不能包含 out 参数。在定义声明和实现声明中都必须包含上下文关键字 partial并且直接放在关键字 void 之前。 可以有定义部分而没有实现部分。
7. 类和继承
类继承
通过继承可以定义一个新类新类纳入一个已经声明的类并进行扩展。
可以使用一个已经存在的类作为新类的基础。已存在的类称为基类base class新类称为派生类 derived class。派生类成员的组成如下 本身声明的成员基类的成员 要声明一个派生类需要在类名后加入基类规格说明。基类规格说明由冒号和用作基类的类名称组成。派生类直接继承自列出的基类。派生类扩展它的基类因为它包含了基类的成员还有它本身声明中的新增功能。派生类不能删除它所继承的任何成员。 例
class OtherClass : SomeClass
{...
}除了特殊的类 object所有的类都是派生类。即使天赋没有基类规格说明。类 object 是唯一的非派生类因为它是继承层次结构的基础。
类继承的注意点
一个类声明的基类规格说明中只能由一个单独的类。这称为单继承。虽然类只能直接继承一个基类但派生的层次没有限制。也就是说作为基类的类可以派生自另外一个类而这个类又派生自另外一个类…直至最终到达 object。
屏蔽基类的成员
虽然派生类不能删除他继承的任何成员但可以用与基类成员名称相同的成员来屏蔽mask基类成员。
注意点
要屏蔽一个继承的数据成员需要声明一个新的相同类型的成员并使用相同的名称。通过在派生类中声明新的带有相同签名的函数成员可以屏蔽继承的函数成员。签名由名称和参数列表组成不包括返回类型。要让编译器知道你在故意屏蔽继承的成员可使用 new 修饰符否则程序可以编译成功但编译器会警告你隐藏了一个继承的成员。也可以屏蔽静态成员。
例
class SomeClass //基类
{public string Field1;
}
class OtherClass : SomeClass //派生类
{new public string Field1; //用同样的名称屏蔽基类成员
}基类访问
如果派生类必须访问被隐藏的继承成员可以使用基类访问base access表达式。基类访问表达式由关键字 base 后面跟着一个点和成员的名称组成如下所示 Console.WriteLine({0},base.Field1);
使用基类的引用
派生类的实例由基类的实例和派生类新增的成员组成。派生类的引用指向整个类对象包括基类部分。 使用类型转换运算符可以把该引用转换为基类类型。
MyDerivedClass derived new MyDerivedClass(); //创建一个对象
MyBaseClass mybc (MyBaseClass) derived; //转换引用基类部分的引用“看不到”派生类对象的其余部分因为它通过基类类型的引用“看”这个对象。
虚方法和覆写方法
虚方法可以使基类的引用访问“升至”派生类内。 可以使用基类引用调用派生类的方法只需满足下面的条件。
派生类的方法和基类的方法由相同的签名和返回类型。基类的方法使用 virtual 标注。派生类的方法使用 override 标注。
例
class MyBaseClass
{virtual public void Print(){...}
}
class MyDerivedClass : MyBaseClass
{override public void Print(){...}
}注意
覆写和被覆写的方法必须有相同的可访问性。不能覆写 static 方法或非虚方法。方法、属性和索引器以及另一种成员类型——事件都可以被声明为 virtual 和 override。
类访问修饰符
类的可访问性有两个级别public 和 internal
标记为 public 的类可以被系统内任何程序集中的代码访问。标记为 internal 的类只能被它自己所在的程序集内的类看到。 这是默认的可访问级别。
程序集间的继承
C#允许从一个在不同的程序集内定义的基类来派生类。 要从不同的程序集中定义的基类派生类必须具备以下条件。
基类必须被声明为 public 这样才能从它所在的程序集外部访问它。必须在 Visual Studio 工程中的 References 节点中添加对包含该基类的程序集的引用。
成员访问修饰符
5个成员访问级别
public公有成员的可访问性限制最少。private私有成员的可访问性访问级别最严格只能被它自己的类的成员访问。不能被其他的类访问包括继承它的类。protected受保护成员的可访问性访问级别如同 private但它允许派生自该类的类访问该成员。即使程序集外部继承该类的类也能访问该成员。internal内部成员的可访问性对程序集内部的所有类可见但对程序集外部的类不可见。protected internal受保护内部成员的可访问性对所有继承该类的类以及程序集内部的所有类可见。是 protected 和 internal 的并集。 必须对每个成员指定成员访问级别。如果不指定它的隐式访问级别是 private。 成员的可访问性不能比它的类高。
成员可访问性总结 抽象成员
抽象成员是指被设计为被覆写的函数成员。 抽象成员有以下特征
必须是一个函数成员。不能是字段和常量。必须用 abstract 修饰符标记。不能有实现代码块。 例
abstract public void PrintStuff(string s); //抽象方法
abstract public int MyProperty //抽象属性
{get;set;
}抽象方法只可以在抽象类中声明。以下4种类型可以声明为抽象的
方法属性事件索引器 派生类中抽象成员的实现必须指定 override 修饰符。
抽象类
抽象类是指设计为被继承的类。 抽象类只能被用作其他类的基类。
不能创建抽象类的实例。抽象类使用 abstract 修饰符声明。抽象类可以包含抽象成员或普通的非抽象成员。抽象类自己可以派生自另一个抽象类。任何派生自抽象类的类必须使用 override 关键字实现该类所有的抽象成员除非派生类自己也是抽象类。
密封类
密封类与抽象类相反不能用作基类可以被实例化。
密封类只能用作独立的类他不能用作基类。密封类使用 sealed 修饰符标注。 例
sealed class MyClass
{...
}静态类
静态类中所有成员都是静态的。静态类用于存放不受实例数据影响的数据和函数。 静态类的一个常见用途可能是创建一个包含一组数学方法和值的数学库。
类本身必须标记为 static。类的所有成员必须是静态的类可以有一个静态构造函数但不能有实例构造函数静态类是隐式密闭的不能继承静态类
扩展方法
扩展方法的要求如下
声明扩展方法的类必须声明为 static扩展方法本身必须声明为 static扩展方法必须包含关键字 this 作为它的第一个参数类型并在后面跟着它扩展的类的名称。 例
//必须是静态类
static class ExtendMyData
{//必须是公有和静态public static double Acerage(this MyData md){...}
}8. 表达式和运算符
9. 语句
using 语句
某些类型的非托管对象有数量限制或很耗费系统资源。在代码使用完它们后尽快释放它们是非常重要的。 using 语句有助于简化该过程并确保这些资源被适当地处置。 资源是指实现了 System.IDisposable 接口的类或结构。IDisposable 接口含有一个名称为 Dispose 的方法。
包装资源的使用
using 语句帮助减少意外的运行时错误带来的潜在的问题, 它整洁地包装了资源的使用。 有两种形式地 using 语句。 第一种形式如下 using (ResourceType IDentifier Expression) Statement
(ResourceType IDentifier Expression)圆括号内的代码分配资源。Statement是使用资源的代码。using 语句隐式产生处置该资源地代码。
意外地运行时错误称为异常可能产生异常的代码放在try块中无论有没有异常都必须执行的代码放进 finally 块中。 这种形式的 using 语句确实是这么做的。
分配资源把 statement 放进 try 块创建资源的 Dispose 方法的调用并把它放进 finally 块中。
例
static void Main(string[] args)
{//打开一个文本文件, 并向其中写入一行using (TextWriter tw File.CreateText(Lincoln.txt)){tw.WriteLine(Four score and seven years ago,...);}//打开相同的文本文件, 一行一行读取并显示它的内容using (TextReader tr File.OpenText(Lincoln.txt)){string InputString;while((InputString tr.ReadLine()) ! null){Console.WriteLine(InputString);}}
}多个资源和嵌套
using 语句还可以用于相同类型的多个资源资源声明用于逗号隔开。 语法如下 using (ResourceType Id1 Expr1, Id2 Expr2, ...) EmbeddedStatement
例
static void Main()
{using (TextWriter tw1 File.CreateText(Lincoln.txt), tw2 File.CreateText(Franklin.txt)){tw1.WriteLine(Four score and seven years ago,...);tw1.WriteLine(Early to bed; Early to rise ...);}using (TextReader tr1 File.OpenText(Lincoln.txt),tr2 File.OpenText(Franklin.txt)){string InputString;while ((InputString tr1.ReadLine()) ! null){Console.WriteLine(InputString);}while ((InputString tr2.ReadLine()) ! null){Console.WriteLine(InputString);}}
}using 语句还可以嵌套。简单语句还可以省略块。
using (TextWriter tw1 File.CreateText(Lincoln.txt))
{tw1.WriteLine(Four score and seven years ago,...);using (tw2 File.CreateText(Franklin.txt)) //嵌套语句tw1.WriteLine(Early to bed; Early to rise ...); //简单语句省略了块
}using 语句的另一种形式
资源在 using 语句之前声明。 using (Expression) EmbeddedStatement
例
TextWriter tw File.CreateText(Lincoln.txt); //声明资源
using (tw) //using 语句tw.WriteLine(Four score and seven years age, ...);虽然这种形式也能确保使用完资源后总是调用 Dispose 方法但他不能防止在 using 语句已经释放了它的非托管资源之后使用该资源这可能会导致状态不一致。因此它提供了较少的保护不推荐使用。
10. 结构
结构是程序员定义的数据类型与类非常类似。它们有数据成员和函数成员。虽然与类相似但是结构有许多重要的区别
类是引用类型而结构是值类型结构是隐式封闭的这意味着不能从它们派生其他结构。 关键字struct 声明语法
struct StructName
{MemberDeclarations
}在声明结构体时不允许使用实例属性和字段初始化语句但是静态属性和静态字段都可以在声明时初始化即使结构体不是静态的。 因为结构是值类型和所有值类型一样结构类型变量含有自己的数据。因此
结构类型的变量不能为 null;两个结构变量不能引用同一对象。
类和结构在内存的安排
//类
class CSimple
{public int X;public int Y;
}
//结构
struct Simple
{public int X;public int Y;
}class Program
{static void Main(){CSimple cs new CSimple();Simple ss new Simple();...}
}对结构和类赋值的区别
把一个结构赋值给另一个结构就是将一个结构的值复制给另一个结构。把一个类赋值给另一个类就是将两个类指向堆中同一对象。
构造函数和析构函数
结构可以有实例构造函数和静态构造函数但不允许有析构函数。
实例构造函数
对于每个结构都存在预定义的无参构造函数而且不能删除或重定义。 但是可以创建有参构造函数。
静态构造函数
与类相似结构的静态构造函数创建并初始化静态数据成员而且不能引用实例成员。结构的静态构造函数和类的静态构造函数的规则一样但允许有不带参数的静态构造函数。 以下两种行为任意一种发生之前将会调用静态构造函数。
调用显式声明的构造函数引用结构的静态成员
11. 枚举
枚举是由程序员定义的类型与类和结构一样。
与结构一样枚举是值类型直接存储它们的数据而不是分开存储成引用和数据。枚举只有一种类型的成员命名的整数值常量。
关键字enum
例
enum TrafficLight
{Green,Yellow,Red
}每个枚举类型都有一个底层整数类型默认为 int。
每个枚举成员都被赋予一个底层类型的常量值。在默认情况下编译器对第一个成员赋值为 0对每一个后续成员赋的值都比前一个成员多 1.
枚举只有单一的成员类型声明的成员常量。
不能对成员使用修饰符。它们都隐式地具有和枚举相同的可访问性。由于成员是静态的即使在没有该枚举类型的变量时也可以访问它们。
设置底层类型和显式值
可以把冒号和类型名放在枚举之后这样就可以使用 int 以外的整数类型。类型可以是任何整数类型。 在枚举声明中的变量名之后使用初始化表达式可以显式地设置一个成员的值。 成员的名称不能重复但是值可以重复。
enum TrafficLight : ulong
{Green 10,Yellow 15, //重复的值Red 15 //重复的值
}位标志
标志字程序员们长期使用单个字single world的不同位作为表示一组开/关标志的紧凑方法。
实现步骤
确定需要多少个位标志并选择一种有足够多位的无符号类型保存它。确定每个位位置代表什么并给它们一个名称。声明一个选中的整数类型的枚举每个成员由一个位位置表示。使用按位或OR运算符在持有该位标志的字中设置适当的位。使用按位与AND运算符或 HasFlag 方法检查是否设置了特定位标志。
例
[Flags]
enum CardDeckSettings : uint
{ SingleDeck 0x01, //位0 LargePictures 0x02, //位1 FancyNumbers 0x04, //位2 Animation 0x08 //位3
}枚举类型 标志字 位标志被“或”在一起↓ ↓ ↓
CardDeckSettings ops CardDeckSettings.SingleDeck | CardDeckSettings.FancyNumbers | CardDeckSettings.Animation;判断标志字是否包含特定的位标志集可以使用枚举类型的 HasFlag 布尔方法。 bool useFancyNumbers ops.HasFlag(CardDeckSettings.FancyNumbers);
12. 数组
Clone 方法
Clone 方法为数组进行浅复制他只创建了数组本身的克隆。
克隆值类型数组会产生两个独立数组。克隆引用类型数组会产生指向相同对象的两个数组。 Clone 方法返回 object 类型的引用他必须被强制转换成数组类型。
int[] intArr1 {1,2,3};
int[] intArr2 (int[]) intArr1.Clone();13. 委托
委托和类一样是一种用户定义类型。但类表示的是数据和方法的集合而委托则持有一个或多个方法以及一系列预定义操作。 关键字delegate
声明委托
//声明
delegate void MyDel(int x);
//创建委托对象
MyDel delVar;
delVar new MyDel(myInstObj.MyM1); //创建委托并保存引用还可以使用快捷语法 delVar myInstObj.MyM1相同的作用。
委托使用步骤
声明一个委托类型。委托声明看上去和方法声明相似只是没有实现块。使用该委托类型声明一个委托变量。创建一个委托类型的对象并把它赋值给委托变量。新的委托对象包含指向某个方法的引用这个方法的签名和返回类型必须跟第一步中定义的委托类型一致。你可以选择为委托对象添加其他方法。这些方法的签名和返回类型必须与第一步中定义的委托类型相同。在代码中你可以像调用方法一样调用委托。在调用委托的时候其包含的每一个方法都会被执行。 委托持有的方法可以来自任何类或结构只要它们的返回类型和签名匹配。 方法的列表称为调用列表调用列表中的方法可以是实例方法也可以是静态方法。 调用委托时会执行器调用列表中的所有方法。
为委托添加方法
使用 运算符。
MyDel delVar inst.MyM1; //创建并初始化
delVar SCL.m3; //增加方法
delVar X.Act; //增加方法此时delVar对象的调用列表里有3个方法分别是inst.MyM1 、SCL.m3 、X.Act。
为委托移除方法
使用 - 运算符。
delVar - SCL.m3; //从委托移除方法注意事项
如果在调用列表中的方法有多个实例- 运算符将从列表最后开始搜索并且移除第一个与方法匹配的实例。试图删除委托中不存在的方法将无效。试图调用空委托会抛出异常。如果委托列表为空则委托为 null。
调用带返回值的委托
如果委托有返回值并且在调用列表中有一个以上的方法会发生下面的情况
调用列表中最后一个方法的值就是委托调用返回的值。调用列表中所有其他方法的返回值都会被忽略。
调用带引用参数的委托
如果委托有引用参数参数值会根据调用列表中的一个或多个方法的返回值而改变。 在调用委托列表中的下一个方法时参数的新值不是初始值会传给下一个方法。
匿名方法
语法
匿名方法表达式的语法包含如下组成部分
delegate 类型关键字参数列表如果语句块没有使用任何参数则可以省略。语句块它包含了匿名方法的代码。
delegate(Parameters){ImplementationCode}使用匿名方法
如下地方使用匿名方法
声明委托变量时作为初始化表达式。组合委托时在赋值语句的右边。为委托增加事件时在赋值语句的右边。
返回值
匿名方法不会显式声明返回值。实现代码本身的行为必须通过返回一个委托的返回类型相同的值来匹配委托的返回类型。如果委托有 void 类型的返回值匿名方法就不能返回值。
delegate int OtherDel(int InParam);
OtherDel del delegate(int x){return x 20;}参数
除了数组参数匿名方法的参数列表必须在如下3方面与委托匹配
参数数量参数类型及位置修饰符 可以通过使圆括号为空或省略圆括号来简化匿名方法的参数列表但必须满足以下两个条件委托的参数列表不包含任何 out 参数匿名方法不适用任何参数。
delegate void SomeDel(int x);
Somedel Sdel delegate{Printmessage();};params 参数
如果委托声明的参数列表包含了 params 参数那么匿名方法的参数列表必须省略 params 关键字。
变量和参数的作用域
参数以及声明在匿名方法内部的局部变量的作用域限制在实现代码的主体之内。
外部变量
与委托的具名方法不同匿名方法可以访问它们外围作用域的局部变量和环境。
外围作用域的变量叫作外部变量outer variable用在匿名方法实现代码中的外部变量称为被方法捕获
捕获变量的生命周期的扩展
只要捕获方法是委托的一部分即使变量已经离开了作用域捕获的外部变量也会一直有效。
Lambda 表达式
C# 3.0 引入了 Lambda 表达式简化了匿名方法的语法。
例
MyDel del delegate(int x) { return x 1; }; //匿名方法
MyDel le1 (int x) { return x 1; }; //Lambda 表达式
MyDel le2 (x) { return x 1; }; //Lambda 表达式
MyDel le3 x { return x 1; }; //Lambda 表达式
MyDel le4 x x 1 ; //Lambda 表达式简化主体
如果只包含一条 return 语句或者一个表达式他就可以简化成只有这一条语句而且 return 关键词也可以省略不写。 简化参数列表编译器可以根据 lambda 表达式转化后的类型推断参数类型。lambda 表达式本身没有类型但可以转换为兼容的委托类型这样编译器就可以依据转换来推断参数类型。如果 lambda 表达式只有一个参数并且可以推断出参数类型那么参数列表的圆括号也可以省略。
事件
语法
访问修饰符 event 委托类型 事件名;
事件的很多部分和委托类似实际上事件就像时专门用于某种特殊用途的简单委托。委托和事件的行为之所以相似是有充分理由的。事件包含了一个私有的委托。
事件提供了对它的私有控制委托的结构化访问。你无法直接访问委托。事件中可用的操作比委托更少对于事件我们只可以添加、删除或调用事件处理程序。事件被触发时它调用委托来一次调用调用列表中的方法。
发布者和订阅者
当很多程序都有一个共同的需求即当一个的顶的程序事件发生时程序的其他部分可以得到该事件已经发生的通知。便可以使用发布者/订阅者模式。
发布者publisher发布某个事件的类或结构其他类可以在该事件发生时得到通知。订阅者subscriber注册并在事件发生时得到通知的类或结构。事件处理程序event handler由订阅者注册到事件的方法在发布者触发事件时执行。触发raise事件 调用invoke或触发fire事件的术语。当事件被触发时所有注册到它的方法都会被依次调用。
源代码组件概览
需要在事件中使用的代码有以下 5 部分
委托类型声明。事件处理程序声明。事件声明。事件注册。触发事件的代码。
声明事件
发布者必须提供事件对象创建对象只需要委托类型和名称。
事件声明在一个类中他需要委托类型的名称任何附加到事件如注册的处理程序都必须与委托类型的签名和返回类型匹配。它声明为 public不能使用对象创建表达式new 表达式来创建它的对象。
class Incrementer
{关键字 委托类型 事件名↓ ↓ ↓public event EventHandler CountedADozen;//声明3个事件public event EventHandler MyEvent1, MyEvent2, MyEvent3;//静态事件public static event EventHandler CountedADozen;
}注意点
事件是成员和方法、属性一样事件是类或结构的成员。由于事件是成员我们不能在一段可执行代码中声明事件他不许声明在类或结构中和其他成员一样。事件成员被隐式自动初始化为 null。
订阅事件
要添加到事件的事件处理程序必须具有与事件的委托相同的返回类型和签名。
使用 运算符为事件添加事件处理程序。事件处理程序可以是以下任意一种 实例方法incrementer.CoutedAdozen IncrementDozensCount; //方法引用形式静态方法incrementer.CoutedAdozen ClassB.CounterHandlerB; //方法引用形式匿名方法incrementer.CoutedAdozen delegate{ DozensCount }Lambda 表达式incrementer.CoutedAdozen () DozensCount; 事件添加方法有3种形式实例方法、静态方法、委托形式的实例方法
mc.CountedADozen new EventHandler(cc.CounterHandlerC); //委托形式和委托一样可以使用匿名方法和 Lambda 表达式来添加事件处理程序。
触发事件
事件成员本身只是保存了需要被调用的事件处理程序。如果事件不被触发则什么都不会发生。
触发事件之前和 null 比较如果事件是 null则表示事件没有事件处理程序不能执行。触发事件的语法和调用方法一样
if(CountedADozen ! null) //确认有方法可以执行CountedADozen(source, args); //触发事件15. 接口
声明接口
接口声明不能包含数据成员和静态成员只能包含方法、属性、事件、索引器类型的非静态成员函数。声明不能包含任何实现代码必须使用分号代替每一个声明的主体。按照惯例名称必须以大写 I 开始。接口可以是任何访问修饰符接口成员是隐式 public 不允许有任何访问修饰符。
接口是引用类型
可以通过把类对象引用强制转换为接口类型来获取指向接口的引用。 有了接口引用就可以使用点语法来调用接口的成员。 例
IIfc1 ifc (IIfc1) mc; //获取接口的引用
ifc.PrintOut(interface); //使用接口的引用调用方法as 运算符
上边的例子中使用的强制转换运算符来获取对象接口的引用还可以使用 as 运算符。 例
IIfc1 ifc mc as IIfc1; //获取接口的引用
if(ifc ! null)ifc.PrintOut(interface); //使用接口的引用调用方法区别
强制转换运算符转换未实现的接口时会抛出一个异常as 运算符转换未实现接口时表达式返回 null不会抛出异常。
多接口
类或结构可以实现任意数量的接口。
16. 转换
is 运算符
is 运算符用来检查转换是否会成功完成。 语法 Expr is TargetType 如果 Expr 可以通过以下方式成功转换为目标类型则运算符返回 true
引用转换装箱转换拆箱转换
17. 泛型
泛型generic可以让多个类型共享一组代码。泛型允许我们声明类型参数化type-parameterized的代码用不同的类型进行实例化。 C# 提供了5种泛型类、结构、接口、委托和方法。除了方法是成员其他都是类型。
泛型类
声明泛型类
和声明普通类型差不多区别如下
在类名之后放置一组尖括号。在尖括号中用逗号分隔的占位符字符串来表示需要提供的类型。这叫作类型参数。在泛型类声明的主体中使用类型参数来表示替代类型。
class SomeClassT1,T2
{public T1 SomeVar;public T2 OtherVar;
}创建构造类型
替代类型参数的真实类型叫作类型实参。 SomeClassshort,int尖括号中提供真实类型来替代类型参数。 编译器接受了类型实参并且替换泛型类主体中的相应类型参数产生了构造类型。
创建变量和实例
SomeClassshort, int myInst; //分配类变量
myInst new SomeClassshort, int(); //分配实例分配类变量时在栈上为 myInst 分配了一个引用值是 null分配实例是在堆上把引用赋值给变量。
类型参数的约束
通过约束可以让编译器知道参数类型可以接受哪些类型。只有符合约束的类型才能替代给定类型参数来产生构造类型。
Where 子句
约束使用 where 子句列出。
每一个有约束的类型参数都有自己的 where 子句。如果形参有多个约束它们在 where 子句中使用逗号分隔。 语法
where TypeParam : constraint, constraint,...例
class MyClassT1,T2,T3 where T2 : Customer where T3 : IComparable
{...
}约束类型和次序
约束类型
约束类型描述类名只有这个类型的类或从它派生的类才能用作类型实参class任何引用类型包括类、数组、委托和接口都可以用作类型实参struct任何值类型都可以用作类型实参接口名只有这个接口或实现这个接口的类型才能用作类型实参new()构造函数约束任何带有无参公共构造函数的类型都可以用作类型实参。
次序
最多只能有一个主约束而且必须放在第一位 可以有任意多的接口名称约束 如果存在构造函数约束则必须放在最后。
泛型方法
泛型方法可以在泛型和非泛型类以及构造和接口中声明。
声明泛型方法
泛型方法具有类型参数列表和可选的约束。 例
public void PrintDataS,T(S p, T t) where S : Person
{...
}调用泛型方法
要调用泛型方法应该在方法调用时提供类型实参
MyMethodshort,int();
MyMethodlong,int();类型推断
编译器可以从方法参数的类型中推断出应用作泛型方法的类型参数的类型。
public void MyMethodT(T myVal){...}
int m 5;
myMethodint(m);
MyMethod(m); //从方法参数中推断类型参数扩展方法和泛型类
扩展方法允许将类中的静态方法关联到不同的泛型类上还允许我们向调用类结构实例的实例方法一样来调用方法。 和非泛型类一样泛型类的扩展方法的要求如下
声明扩展方法的类必须声明为 static扩展方法本身必须声明为 static扩展方法必须包含关键字 this 作为它的第一个参数类型并在后面跟着它扩展的类的名称。 例
static class ExtendHolder
{public static void PrintT(this HolderT h){T[] vals h.GetValues();}
}泛型结构
与泛型类相似泛型结构可以有类型参数和约束。泛型结构的规则和条件与泛型类是一样的。 例
struct PieceOfDataT
{public PieceOfData(T value){ _data value; }private T _data;public T Data{get{ return _data; }set{ _data value;}}
}泛型委托
泛型委托和非泛型委托非常相似不过类型参数决定了能接受什么样的方法。
要声明泛型委托在委托名称之后、委托参数列表之前的尖括号中放置类型参数列表。注意有两个参数列表委托形参列表和类型参数列表类型参数的范围包括 返回类型形参列表约束子句 例 返回类型 类型参数 委托形参↓ ↓ ↓
delegate R MyDelegateT,R(T value);泛型接口
泛型接口允许我们编写 形参和接口成员返回类型是泛型类型参数的接口。 例
interface IMyIfcT //泛型接口
{T ReturnIt(T inValue);
}class SimpleS : IMyIfcS //泛型类
{public S ReturnIt(S inValue) //实现泛型接口{return inValue;}
}泛型接口的实现必须唯一
实现泛型类型接口时必须保证类型实参的组合不会在类型中产生两个重复的接口。 错误示例
interface IMyIfcT //泛型接口
{T ReturnIt(T inValue);
}class SimpleS : IMyIfcint,IMyIfcS //错误
{public int ReturnIt(int inValue) //实现第一个接口{return inValue;}public S ReturnIt(S inValue) //实现第二个接口{return inValue;}
}对于泛型接口使用两个相同接口本身没有错问题在于这样会有潜在冲突。如果把 int 作为类型实参来代替第二个接口中的 S 的话Simple 可能会有两个相同类型的接口这是不允许的。
逆变和协变
协变和逆变只能用在接口或者委托中。
协变
官方定义 能够使用比原始指定的派生类型的派生程度更大更具体的类型。 如果存在一个泛型接口InterfaceT它的泛型参数子类类型 InterfaceChinese 可以安全地转换为泛型父类类型 InterfaceHuman这个过程就称为协变。
每一个变量都有一种类型你可以将派生类型的对象赋值给基类型的变量这叫作赋值兼容性。
class Animal
{public int NumberOfLegs 4;
}
class Dog : Animal
{}
class Program
{static void Main(){Animal a1 new Animal();Animal a2 new Dog();}
}仅将派生类型用作输出值与构造委托有效性之间的常数关系叫作协变。为了让编译器知道这是我们的期望必须使用 out 关键字标记委托声明中类型参数。
delegate T Factoryout T();协变关系允许程度更高的派生类型处于返回及输出位置
逆变
官方定义 能够使用比原始指定的派生类型的派生程度更新更抽象的类型。 如果存在一个泛型接口 IBarT它的泛型参数父类类型 IBarHuman可以安全地转换为泛型子类类型 IBarChinese这种期望传入基类时允许传入派生对象的特性叫作逆变。 逆变的关键字是 in 。
delegate void Action1in T(T a);18. 枚举器和迭代器
枚举器和可枚举类型
使用 foreach 语句
当为数组使用 foreach 语句时这个语句会依次取出数组中的每一个元素允许我们读取它的值。 为什么数组可以这么做原因是数组可以按需提供一个叫作枚举器的对象。枚举器可以依次返回请求的数组中的元素。 对于枚举器的类型而言必须有一种方法来获取它获取对象枚举器的方法是调用对象的 GetEnumerator 方法。实现 GetEnumerator 方法的类型叫作可枚举类型enumerable type 或 enumerable。 数组是可枚举类型。
foreach 结构设计用来和可枚举类型一起使用只要给它的遍历对象是可枚举类型比如数组他就会执行如下行为
通过调用 GetEnumerator 方法获取对象的枚举器。从枚举器中请求每一项并且把它作为迭代变量代码可以读取该变量但不可以改变。 必须是可枚举类型↓
foreach(Type varName in EnumerableObject)
{...
}IEnumerator 接口
实现了 IEnumerator 接口的枚举器包含3个函数成员Current、MoveNext以及Reset。
Current 是返回序列中当前位置项的属性。 它是只读属性它返回 object 类型的引用所以可以返回任何类型的对象。 MoveNext 是把枚举器位置前进到集合中下一项的方法。它是返回布尔值指示新的位置是有效位置还是已经超过了序列的尾部。 如果新的位置是有效的方法返回 true。如果新的位置是无效的比如当前位置到达了尾部方法返回 false。枚举器的原始位置在序列中的第一项之前因此 MoveNext 必须在第一次使用 Current 之前调用。 Reset 是把位置重置为原始状态的方法。
手动 foreach 语句
static void Main()
{int[] arry { 11, 22, 33, 44 }; IEnumerator ie arry.GetEnumerator(); while (ie.MoveNext()) //移到下一项 { int item (int)ie.Current; //获取当前项 Console.WriteLine(item); } ie.Reset(); //重置为原始位置
}IEnumerable 接口
可枚举类是指实现了 IEnumberable 接口的类。IEnumerable 接口只有一个成员——GetEnumerator 方法它返回对象的枚举器。
使用 IEnumerable 和 IEnumerator 示例
public class MyColors : IEnumerable
{ private string[] Colors { red, yellow, blue, black }; public IEnumerator GetEnumerator() { return new ColorEnumerator(Colors); }
} public class ColorEnumerator : IEnumerator
{ private string[] colors; private int position -1; public ColorEnumerator(string[] theColors) //构造函数{ colors new string[theColors.Length]; for (int i 0; i theColors.Length; i) { colors[i] theColors[i]; } } public bool MoveNext() //实现MoveNext{ if (position colors.Length - 1) { position; return true; } else return false; } public void Reset() //实现Reset{ position -1; } public object Current //实现Current{ get { if (position-1) { throw new InvalidOperationException(); } if (positioncolors.Length) { throw new InvalidOperationException(); } return colors[position]; } }
}使用这个类
public static void Main(string[] args)
{ MyColors mc new MyColors(); foreach (var color in mc) { Console.WriteLine(color); }
}输出结果
red
yellow
blue
black泛型枚举接口
实际上在大多数情况下应该使用泛型版本IEnumerableT和IEnumeratorT。
这些是协变接口所以它们的实际声明是IEnumerableout T和IEnumeratorout T。IEnumeratorT的类实现了 Current 属性它返回实际类型的实例而不是 object 基类的引用。 泛型接口的枚举器是类型安全的它返回实际类型的引用。
迭代器
除了自己创建枚举器和可枚举类C#从2.0版本就提供更简单的方式编译器为我们创建他们这种结构叫作迭代器iterator。
迭代器块
迭代器块是有一个或多个 yield 语句的代码块。 以下都可以是迭代器块
方法主体访问器主体运算符主体 迭代器块与其他代码块不同。其他块包含的语句被当作是命令式的。也就是说先执行代码块的第一个语句然后执行后面的语句最后控制离开块。 迭代器块不是需要在同一时间执行的一串命令式命令而是声明性的它描述了希望编译器为我们创建的枚举器类的行为。迭代器块中的代码描述了如何枚举元素。 迭代器块中有两个特殊语句yield return 语句指定了序列中返回的下一项。yield break 语句指定在序列中没有其他项。
使用迭代器来创建枚举器
public class Iter
{ public IEnumeratorstring GetEnumerator() { return BlackAndWhite(); //返回枚举器 } public IEnumeratorstring BlackAndWhite() //迭代器 { yield return Black; yield return White; }
}执行
public static void Main(string[] args)
{ Iter i new Iter(); foreach (var co in i) { Console.WriteLine(co); }
}执行结果
Black
White使用迭代器创建可枚举类型
public class Iter
{ public IEnumeratorstring GetEnumerator() { IEnumerablestring myEnumerable BlackAndWhite(); //获取可枚举类型 return myEnumerable.GetEnumerator(); //返回枚举器 } public IEnumerablestring BlackAndWhite() //迭代器 { yield return Black; yield return White; }
}执行
public static void Main(string[] args)
{ Iter i new Iter(); //使用类对象 foreach (var co in i) { Console.WriteLine(co); } //使用类枚举器方法 foreach (var co in i.BlackAndWhite()) { Console.WriteLine(co); }
}执行结果
Black
White
Black
White迭代器的实质
迭代器需要 System.Collections.Generic 命名空间用 using 指令引入它。 编译器生成的枚举器不支持 Reset 方法。 编译器生成的枚举器类是包含4个状态的状态机。
Before 由此调用 MoveNext 之前的初始状态。Running 调用 MoveNext 后进入这个状态。在这个状态中枚举器检测并设置下一项的位置。在遇到 yield return 、yield break 或在迭代器体结束时退出状态。Suspended 状态机等待下次调用 MoveNext 的状态。After 没有更多项可以枚举的状态。 如果状态机在 Before 或 Suspended 状态时调用了 MoveNext 方法就转到了 Running 状态。在 Running 状态中他检测集合的下一项并设置位置。 如果有更多项状态机会转入 Suspended 状态如果没有更多项他转入并保持在 After 状态。
19. LINQ
LINQ发音link代表语言集成查询Language Integrated Query。 LINQ是 .NET 框架的扩展允许我们以使用 SQL 查询数据库的类似方式来查询数据集合。 使用LINQ可以从数据库、对象集合以及 XML 文档等中查询数据。
查询的定义是带有 from 和 select 关键字的语句。 例
int[] num { 2, 13, 5, 16 };
IEnumerableint lowNums from n in num where n 10 select n;
foreach (int i in lowNums)
{ Console.WriteLine(i);
}匿名类型
创建无名类类型的特性叫作匿名类型。
匿名类型只能用于局部变量不能用于类成员。由于匿名类型没有名字我们必须使用 var 关键字作为变量类型。不能设置匿名类型对象的属性。编译器为匿名类型创建的属性是只读的。 例
//1.赋值形式成员初始化语句
var student new { Name Qimeile, Age 12, Major History };
//2.投影初始化语句形式
class Other
{ public static string Name Qimeile;
}
string Major History;
//Age是赋值形式Other.Name是成员访问Major是标识符
var student2 new { Other.Name, Age 19, Major };方法语法和查询语法
LINQ 查询时可以有两种形式的语法查询语法和方法语法。 查询语法是声明式的查询描述的是你想返回的东西但并没有指明如何执行这个查询 方法语法是命令式的它指明了查询方法调用的顺序。
int[] numbers {2,4,5,28,13,30,1,64,33};
//查询语法
var numsQuery from n in numberswhere n 20select n;
//方法语法
var numsMethod numbers.Where(N N 20 );
//两种形式的组合
int numsCount (from n in numberswhere n 20select n).Count();查询变量
LINQ 查询可以返回两种类型的结果
可枚举的一组数据它是满足查询参数的项列表标量单一值他是满足查询条件的结果的某种摘要形式。 例
Listint numbers new Listint() { 2, 4, 5, 28, 13, 30, 1, 64, 33 };
//查询语法 返回枚举器
IEnumerableint numsQuery from n in numbers where n 20 select n;
//返回标量
int numsCount (from n in numbers where n 20 select n).Count();
foreach (var i in numsQuery)
{ Console.Write(i );
}
Console.WriteLine(满足条件的数量为 numsCount);
Console.WriteLine(加入新的满足条件数值);
numbers.Add(3);
foreach (var i in numsQuery)
{ Console.Write(i );
}
Console.WriteLine(满足条件的数量为 numsCount);执行结果
2 4 5 13 1 满足条件的数量为 5
加入新的满足条件数值
2 4 5 13 1 3 满足条件的数量为 5通过例子中的结果可以发现
numsQuery 查询变量不会包含查询的结果编译器会创建能够执行这个查询的代码。查询变量 numsCount 包含的是真实的整数值他只能通过真实运行查询后获得。查询执行时间的差异可以总结如下 如果查询表达式返回枚举则查询一直到处理枚举时才会执行。如果枚举被处理多次查询就会执行多次。如果在遍历之后、查询执行之前数据有改动则查询会使用新的数据。如果查询表达式返回标量查询立即执行并且把结果保存在查询变量中。
查询表达式的结构
子句必须按照一定的顺序出现。 from 子句和 select ... group 子句这两部分是必需的其他子句是可选的。 LINQ中 select 子句在表达式最后。这与 SQL 的 SELECT 语句在查询的开始处不一样。C# 这么做的原因之一是让 Visual Studio 智能感应能在我们输入代码时提供更多选项。 可以有任意多的 form ... let ... where 子句。
from 子句
from 子句指定了要作为数据源使用的数据集合。他还引入了迭代变量。迭代变量逐个表示数据源的每一个元素。 语法from Type Item in Items
join 子句
联结的重要事项
使用联结来结合两个或更多集合中的数据联结操作接受两个集合然后创建一个临时的对象集合其中每一个对象包含两个原始集合对象中的所有字段。
标准查询运算符
标准查询运算符能让我们查询任何.NET数组或集合。 重要特性
标准查询运算符使用方法语法。一些运算符返回 Ienumerable 对象或其他序列而其他运算符返回标量。标量立即执行并返回一个值而不是可枚举类型对象。ToArray()、ToList()等 ToCollection 运算符也会立即执行。被查询的集合对象叫作序列他必须实现 IEnumerableT 接口其中T是类型。 LINQ to XML
LINQ 为XML增加了一些特性
可以使用单一语句自顶向下创建 XML 树。可以在不适用包含树的 XML 文档的情况下在内存中创建并操作 XML。可以在不适用 Text 子节点的情况下创建和操作字符串节点。最大改进是在搜索一个 XML 树时不再需要遍历它只需要查询树并让它返回结果。
XML 基础
XML标签区分大小写 示例 personspersonname张三/namelength 175cm/length/personpersonname李四 /namelength200cm/length/person
/personsXML 类
LINQ to XML 可以有两种方式用于 XML。
简化的 XML 操作 APILINQ 查询工具
20. 异步编程
什么是异步
启动程序时系统会在内存中创建一个新的进程。进程是构成运行程序的资源的集合。 在进程内部系统创建了一个称为线程的内核kernel对象。
关于线程
默认情况下一个进程只包含一个线程从程序的开始一直到执行到结束。线程可以派生其他线程一个进程可能包含不同状态的多个线程。如果一个进程有多个线程它们将共享进程的资源。系统为处理器执行所调度的单元是线程不是进程。
async/await
异步的方法在完成其所有工作之前就返回到调用方法。
调用方法calling method该方法调用异步方法然后再异步方法执行其任务的时候继续执行。异步async方法该方法异步执行其工作然后立即返回到调用方法。await 表达式用于异步方法内部指明需要异步执行的任务。一个异步方法可以包含任意多个 await 表达式如果一个都不包含的话编译器会发出警告。
异步方法
异步方法的头方法中必须包含 async 关键字且必须位于返回类型之前。async 关键字是一个上下文关键字除了作为方法修饰符之外还可以用作标识符。异步方法的返回类型 Task不需要返回值但需要检查异步方法的状态TaskT调用方法通过读取 Task 的Result 属性来获取这个 T 类型的值。ValueTaskT这是一个值类型对象他与 TaskT 类似但用于任务结果可能已经可用的情况。因为它是值类型所以它可以放在栈上而无须像 TaskT 对象那样再堆上分配空间。某种情况下可以提高性能。void调用方法仅想执行异步而不需要与他做任何进一步的交互时。任何具有可访问的 GetAwaiter 方法的类型。
await 表达式
await 表达式指定了一个异步执行的任务由 await 关键字和一个空闲对象称为任务可能是 Task 类型也可能不是组成。默认情况下这个任务在当前线程上异步执行。 一个空闲对象即是 awaitable 类型的实例。awaitable 类型包含了 GetAwaiter 方法该方法没有参数返回一个 awaiter 类型的对象。 awaiter 类型包含如下成员
bool IsCompleted { get; }void OnCompleted(Action) 同时还包含以下成员之一void GetResult();T GetResult();T 为任意类型 实际上不需要构建自己的 awaitable而是使用 Task 类或 ValueTask 类它们是 awaitable 类型。通过 Task.Run 方法来创建 Task其签名如下。其中 FuncTReturn 是一个预定义委托不包含任何参数返回类型为 TReturn。
取消一个异步操作
System.Threading.Tasks 命名空间中有两个类被设计为取消异步操作分别为 CancellationToken 和 CancellationTokenSource。
CancellationToken 包含一个任务是否应被取消的信息 IsCancellationRequested。拥有 CancellationToken 对象的任务需要定期检查其令牌状态如果 IsCancellationRequested 属性为 true任务需停止其操作并返回。CancellationToken 不可逆且只能使用一次。即一旦 IsCancellationRequested 被设置为 true就不能更改。CancellationTokenSource 对象创建可分配给不同任务的 CancellationToken 对象任何持有 CancellationTokenSource 的对象都可以调用其 Cancel 方法这将使 IsCancellationRequested 被设置为 true。
异常处理和 await 表达式
await 表达式可以像其他表达式一样放在 try 语句内try … catch … finally 结构将按你期望的那样工作。 C#6.0 后也可以在 catch 和 finally 块中使用 await 表达式。在异常不需要终止应用程序时可以使用 await 来记录日志或运行其他时间较长的任务。如果新的异步任务也产生了一场则任何原有的异常信息都将丢失。
在调用方法中同步地等待任务
使用 Wait 方法可以等待 Task 对象完成。 Wait 方法用于单一 Task 对象。 可以使用 Task 类中的静态方法等待一组 Task 对象完成。
WaitAll 等待一组 Task 对象全部完成。WaitAny等待一组中至少一个 Task 对象完成。 WaitAll 和 WaitAny 分别包含 4 个重载除了完成任务之外还允许以不同的方式继续执行如设置超时时间或使用 CancellationToken 来强制执行处理的后续部分。 在异步方法中异步地等待任务
使用 Task.WhenAll 和 Task.WhenAny 异步等待多个 Task。这两个方法称为组合子combinator。
Task.Delay 方法
Task.Delay 方法创建一个 Task 对象该对象将暂停其在线程中的处理并在一定时间之后完成。
Thread.Sleep阻塞线程。Task.Delay不阻塞线程线程可以继续处理其他工作。
例
public class Simple
{ private Stopwatch sw new Stopwatch(); public void DoRun() { Console.WriteLine(Caller: Before call); ShowDelayAsync(); Console.WriteLine(Caller: After call); } private async void ShowDelayAsync() { sw.Start(); Console.WriteLine(Before Delay:sw.ElapsedMilliseconds); await Task.Delay(1000); Console.WriteLine(After Delay:sw.ElapsedMilliseconds); }
}
public class Program
{ public static Main(string[] args) { Simple ds new Simple(); ds.DoRun(); Console.ReadLine(); }
}执行结果
Caller: Before call
Before Delay:0
Caller: After call
After Delay:1003Task.Yield
Task.Yield 方法创建一个立即返回的 awaitable。 等待一个 Yield 可以让异步方法在执行后续部分的同时返回到调用方法。可以将其理解成离开当前的消息队列回到队列末尾让处理器有时间处理其他任务。
使用异步 Lambda 表达式
除了异步方法还可以使用异步匿名方法和异步 Lambda 表达式。 例
startWorkButton.Click async (sender, e) { ... };BackgroundWorker 类
可以使用 BackgroundWorker 类创建后台线程。
并行循环
任务并行库Task Parallel Library 这里只介绍两个
Parallel.For 循环Parallel.ForEach 循环
其他异步编程
当委托调用时他的调用列表只有一个方法它就可以异步执行这个方法。
BeginInvoke 参数列表引用方法需要的参数callback 参数state 参数BeginInvoke 从线程池中获取一个线程并且引用方法在新的线程中开始运行返回给调用线程一个实现 IAsyncResult 的接口的对象的引用。这个接口引用包含了在线程池中运行的异步方法的当前状态。然后原始线程可以继续执行。 EndInvoke 参数列表IAsyncResult 对象的引用EndInvoke 方法用来获取由异步方法调用返回的值并且释放线程使用的资源。EndInvoke 提供了异步方法调用的所有输出包括 ref 和 out 参数如果委托的引用方法有 ref 或 out 参数则它们必须包含在 EndInvoke 的参数列表中并且在 IAsyncResult 对象引用之前。如long result del.EndInvoke(out someInt, iar);。
异步方法调用的三种标准模式 在等待直到完成模式中在发起了异步方法以及做了一些其他处理之后原始线程就中断并且等异步方法完成之后再继续。在轮询模式中原始线程定期检查发起的线程是否完成如果没有则可以继续做一些其他的事情。在回调模式中原始线程一直执行无须等待或检查发起的线程是否完成。
21. 命名空间和程序集
22. 异常
23. 预处理指令
24. 反射和特性
特性一定使用了反射反射不一定用到特性。
元数据和反射
元数据有关程序及其类型的数据。 反射运行中的程序查看本身的元数据或其他程序的元数据的行为。
Type 类
type 是一个抽象类CLR 创建从 Type 派生的类的实例Type 包含了类型信息。
不管创建的类型有多少个实例只有一个 Type 对象会关联到所有这些实例。每一个类型CLR 都会创建一个包含这个类型信息的 Type 类型的对象。
System.Type 类的部分成员
成员成员类型描述Name属性返回类型的名字Namespace属性返回包含类型声明的命名空间Assembly属性返回声明类型的程序集。如果是泛型返回定义这个类型的程序集。GetFields方法返回类型的字段列表GetProperties方法返回类型的属性列表GetMethdos方法返回类型的方法列表
获取 Type 对象
GetType 方法object 类型包含 GetType 方法它返回实例的 Type 对象的引用。Type t myInstance.GetType();typeof 运算符提供类型名作为参数返回 Type 对象的引用。Type t typeof( DerivedClass );
什么是特性
特性attribute是一种允许我们向程序的程序集添加元数据的语言结构。它是用于保存程序结构信息的特殊类型的类。
应用了特性的程序结构叫作目标target。设计用来获取和使用元数据的程序叫作特性的消费者consumer 按照习惯特性名使用 Pascal 命名法并且以 Attribute 后缀结尾。
应用特性
特性的目的是告诉编译器把程序结构的某组元素嵌入程序集。可以通过把特性应用到结构来实现。
通过在结构前放置特性片段来应用特性。特性片段有方括号包围特性名和有时候参数列表构成。 例
[Serializable] //特性
public class MyClass{ ... }
[MyAttribute(Simple class,Version 3.5)] //带参数的特性
publc class MyOtherClass{ ... }预定义的保留特性
Obsolete 特性
这个特性标记了不应被使用的程序实体。
参数 message是一个字符串描述项目为什么过时的原因以及该替代使用什么。参数 iserror是一个布尔值。如果该值为 true编译器应把该项目的使用当作一个错误。默认值是 false编译器生成一个警告。
Conditional 特性
允许我们包括或排斥特定方法的所有调用。 使用规则
该方法是类或结构体的方法必须是 void 类型不能声明为 override但可以标记为 virtual。该方法不能是接口方法的实现。