目录
写在前面
最好的学习方法是跟项目,查官方文档:
C#语言MSDN文档
构成C#语言的基本元素
-
关键字
- 参考自官网资料:C#关键字
上下文关键字是针对某些上下文的语境里边是关键字,出了这些上下文就不是关键字了。
- 参考自官网资料:C#关键字
-
操作符
查阅官方文档:C# 运算符和表达式(C# 参考) -
标识符
- 需要定义合法的标识符
- 不能是关键字,不能以数字开头
- 可以是字母数组下划线
- 在命名
class
、Interface
、struct
或delegate
类型时,使用Pascal
大小写(“PascalCasing
”) - 在命名
private
或internal
字段时,使用驼峰式大小写(“camelCasing
”),并对它们添加 _ 作为前缀。 命名局部变量(包括委托类型的实例)时,请使用驼峰式大小写。
-
标点符号
-
文本
-
注释与空白
整理格式的快捷键:Ctrl+E,Ctrl+D
初识类与名称空间
- 类构成程序的主题
- 名称空间以树形结构组织类
- 类和名称空间都是放在类库(DLL)里的
- 新建项目时的模板,就是在创建项目的时候,主要的区别时那个模板给引用了不同的类库以实现不同的功能
类库的引用方式
- 一个类库必须要有自己的说明文档,里边有哪些名称空间,有哪些类,有哪些方法,这些方法是干嘛的
- 优秀的程序追求:高内聚,低耦合
- 学习UML类图
DLL引用(黑盒引用,无源代码)
- 右击引用,添加引用,浏览,可以选择本地的类库(别人发的)
- 这种是看不到源码的 ,如果其中的方法有无,无法自己进行修改
- 会导致对类库的依赖关系
- 也可以使用Nuget添加引用(用于解决比较复杂的依赖关系,想要引用一个类库,但是这个类库又引用着其他类库)
项目引用(白盒引用,有源代码)
- 右击引用,添加引用,在Solution里添加项目引用(Projects)(首先需要将这个project包含在当前的solution里边,方法右击解决方案,添加现有项目)
C#的类型系统
- C#是一个强类型语言,而C语言是弱类型语言,比如说
if()...else
语句,在C语言中if里边的值只要是一个逻辑值就可以,甚至可以是数字,因为在C语言中0代表false,非0代表true,如在C语言中if(1){...}
这样的写法是正确的,但是在C#中是错误的,在C#中,只能是if(true){...}
这样的表达,里边的只能是true或者false。 - C#对弱类型语言的模仿(dynamic)
dynamic var = 100;
Console.WriteLine(var);
var = "John";
Console.WriteLine(var);
这样var既可以接收int类型,在需要时也可以接收其他类型;
获取某个对象的类型
Form f = new Form();
Type t = f.GetType();
Console.WriteLine(t.Name); // Form
动态获取某个类型的属性或方法(常用于反射)
Type type = typeof(Form);
PropertyInfo[] p = type.GetProperties(); // 属性
MethodInfo[] mi = type.GetMethods(); // 方法
某个类型变量在内存中的分布(堆内存和栈内存)
- C#中不用手动释放内存,在C#机制中有一个垃圾收集器
1 栈内存
- 栈内存比较小,速度很快
- 局部变量在栈上分配内存
- 通常是给函数调用使用的,使用不当会造成
StackOverflow
------ 如下,如果调用这个函数就会造成栈内存溢出public void BadMethod() { int x = 100; this.BadMethod(); }
- 使用
stackalloc
比较直观的体验一下栈溢出注意:在C#中是有指针的,不过不推荐使用,如果要使用,需要添加unsafe关键字(有两种方式) public class Program { static unsafe void Main(string[] args) { int* p = stackalloc int[999999999]; } } 或者 public class Program { static void Main(string[] args) { unsafe { int* p = stackalloc int[999999999]; } } }
程序一旦执行,就会使栈内存溢出(内存不够用了)
2 堆内存
- 堆内存很大,速度相对较慢
- 实例永远是分配在堆内存里
C#的五大数据类型
包括:类类型、结构体类型、枚举类型、接口类型、委托类型
图片来自于刘铁猛老师的C#课程
右边虚线以上(蓝色的)是真正的数据类型,非常常用,C#已经将他们吸收为自己的关键字了,以下是关键字,用这个关键字来定义自己的数据类型。
第一列关键字是和引用类型相关的,第二列是和值类型相关的
蓝色的都是基本数据类型,很常用的
变量
- 变量名称对应着变量的值在内存中的存储位置
- 变量 = 以变量名所对应的内存地址为起点,以其数据类型所要求的存储空间为长度的一块内存区域
- 一共有七种变量:
静态变量
局部变量(成员变量、字段)
数组元素
值参数
引用参数ref
输出参数out
局部变量
1 值类型变量(以结构体类型byte、sbyte、short、ushort为例)
- 值类型的变量直接存储数据值,包括整数类型,浮点类型,布尔类型和枚举类型(它是一种特殊的值类型)
- 整数类型:
sbyte
short
int
long
byte
ushort
uint
ulong
- 浮点类型:
float
double
; 含小数点默认都是double类型,如果要指明为float,要加上F或者f。如果要强制转化为double,就需要加上d或者D - 布尔类型,其值只能是true或者false
- 值类型的变量不能为null
- 所有值类型均派生自
System.ValueType
- 每种值类型均有一个隐式的默认构造函数来初始化该类型的默认值:
int i = new int(); int32i = new int32(); // 等价 int i = 0; int32 i = 0; 上述四种写法是等价的
2 引用类型变量
- 对于引用类型,无论是何种引用类型,计算机只要一看到是引用类型,就会立即分配4个字节
仅是这样声明一下,都会分配四个字节 static void Main(string[] args) { 这是一个局部变量,所以分配的4个字节是栈内存上的 Student stu; } . . . class Student { int ID; int score; }
- 用new创建的对象实例,这时才会给
ID
和socre
分配内存,存储在堆内存中,这个地址保存在stu
中 - 堆栈是一种由系统弹性配置的内存空间,没有特定的大小和存活时间
- 必须使用new来创建引用类型的变量
- 必须在托管堆内存中为引用类型变量分配内存
- 引用类型的变量是由垃圾回收机制来管理的
- 所有被称为类的都是引用类型,包括类,接口,数组和委托
- 多个引用类型变量可以引用同一对象,这时对一个变量进行修改,其他也会跟着改变
3 值类型和引用类型的区别
- 值类型是在栈中操作;引用类型是在堆中操作
- 而栈在编译时就会分配好空间,而堆内存时在程序运行时动态分配的
4 装箱
- 将值类型转换为引用类型的过程叫做装箱
- 装箱允许将值类型隐式转换为引用类型,将值类型的变量复制到装箱的得到的对象中,装箱后改变值类型变量的值不会改变装箱对象的值:
int i = 100; Object obj = i; //装箱 Console.WriteLine("i:{0},obj:{1}",i,obj); i = 200; Console.WriteLine("i:{0},obj:{1}", i, obj);
- 装箱:当这个引用类型的变量所要引用的值不是堆上的实例,而是栈上的值时,他会复制这个栈上的值,然后在堆内存中找一片区域存放这个值,再把这个区域的地址给那个引用类型的变量;简单来说,装箱就是把栈上的东西搬到堆上的实例
- 装箱会损失程序的性能,因为涉及到堆内存和栈内存的交换
5 拆箱
-
将引用类型转换为值类型的过程叫做拆箱
-
拆箱就是堆上的实例按照要求存储到栈上
-
拆箱允许将引用类型显示转换为值类型,拆箱后得到的值类型数据和装箱对象相等,在执行拆箱操作时,必须符合类型一致原则(类型的兼容性,如不能把string的Object类型转换为int类型):
int i = 100; Object obj = i; //装箱 Console.WriteLine("i:{0},obj:{1}",i,obj); int j = (int)obj; //拆箱 Console.WriteLine("j:{0},obj:{1}", j, obj);
将一个数保留到小数点后两位
double x = 3.1415926535;
double y = Math.Round(x,2); // 将X保留到小数点后两位
1 字符串类
1.1 去掉字符串中的所有空格
String str = "1 2 3 4 5 6 K LP WJ";
for (int i = 0; i<str.Length;i++)
{
if (str[i] == ' ')
{
str = str.Remove(i,1);
i--;
}
}
注意,这里的 i- - 非常重要,它的意思是当删除一个空格后,空格之后的所有字符会一次前移一个位置,删除之前 空格的下一个位置就变成了删除后的 i 的当前位置,不过不进行减一操作的话,在for里还会将 i 自增一次,那么就会错过对删除之后当前 i 位置上字符的判断。
1.2 将字符串中的每个字符颠倒输出
string str = "123456";
for (int i = 0; i<str.Length; i++)
{
char temp = str[0];
str = str.Remove(0, 1);
str = str.Insert(str.Length-i, temp.ToString());
}
直接画个图理解一下:
i | 序列 | str.length-i |
---|---|---|
0 | 2 3 4 5 6 1 | 6 |
1 | 3 4 5 6 2 1 | 5 |
2 | 4 5 6 3 2 1 | 4 |
3 | 5 6 4 3 2 1 | 3 |
4 | 6 5 4 3 2 1 | 2 |
5 | 6 5 4 3 2 1 | 1 |
1.3 从字符串中分离文件路径、文件名、文件后缀名
string str = "D:\\MyFiles\\C#\\新建文本文档.txt";
string[] strs = str.Split('\\','.');
string suffix = strs[strs.Length - 1];
string fileName = strs[strs.Length - 2];
string path = str.Substring(0,str.Length- suffix.Length-fileName.Length-2);
文件名及后缀名永远是字符串的最后两个位置上的字符串。
1.4 从控制台输入一组数字,用空格分开,将其放在一个int数组中
// 获取用户输入
string userInput = Console.ReadLine();
// 拆分用户输入
string[] inputx = userInput.Split(' ');
// 将用户输入的字符串转换成int数组
int[] inputy = new int[inputx.Length];
for (int i = 0; i < inputy.Length; i++)
{
inputy[i] = Convert.ToInt32(inputx[i]);
}
2 数组
2.1 获取二维数组的行数和列数
// 法一 使用GetLength方法
int[,] layerTwo = { { 1, 2, 3 }, { 3, 4, 5 } };
int lineNum = layerTwo.GetLength(0);
int columnNum = layerTwo.GetLength(1);
// 法二 使用数组的Length和Rank属性
int[,] layerTwo = { { 1, 2, 3 }, { 3, 4, 5 } };
int lineNum = layerTwo.Rank;
int columnNum = layerTwo.Length / lineNum;
2.2 ArrayList类
- 相当于一种高级的动态数组
- 可以动态增加和删除元素
- 可自动扩容
- 提供将只读和固定大小包装返回到集合的方法
- 只能是一维的
- 支持null作为其有效值,并且允许有重复的元素
三种构造器:
public ArrayList();
ArrayList list = new ArrayList();
public ArrayList(ICollection);
int[] arr = {1,2,3};
ArrayList list = new ArrayList(arr);
public ArrayList(int n);
int n = 3; // 指定大小初始化内部的数组
ArrayList list = new ArrayList(n);
ArrayList常用的属性
属性 | 说明 |
---|---|
Capacity | 获取或设置可包含的元素个数 |
Count | 获取其实际包含的原色个数 |
IsReadOnly | 是否可读 |
Item | 获取或设置指定索引处的元素 |
添加元素的方法:
public virtual int Add(Object value); // 返回添加到的位置的索引
public virtual void Insert(int index, object value);
public virtual void InsertRange(int index, ICollection c); // 如:可以添加一个数组进去
int[] arr = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
ArrayList list = new ArrayList(arr);
int[] bv = { 989, 89, 98 };
list.InsertRange(list.Count, bv); // 在list结尾处添加了bv数组
删除元素的方法:
public virtual void Clear(); // 删除所有元素
public virtual void Remove(Object value); // 删除特定对象value的第一个匹配项,如果不存在这个对象,则list不发生任何改变
public virtual void RemoveAt(int index); // 删除索引位置上的元素
public virtual void RemoveRange(int index,int count); // 删除从索引index开始的count个元素
查找元素的方法:
public virtual bool Contains(object item); //确定list中是否存在item元素
public virtual int IndexOf(object? value); // 返回value在list中的索引,没有则为-1
public virtual int IndexOf(object? value, int startIndex); // 从索引startIndex开始找,返回value在list中的索引,注意这个索引仍是相对第一个元素而言的,不是相对startIndex而言的
public virtual int IndexOf(object? value, int startIndex, int count); 从索引startIndex开始的count个元素里找
2.3 哈希表(HashTable)
- 哈希表表示的是键/值对的集合
- 其容量可以自动扩容
常用的两个构造函数
public Hashtable();
public Hashtable(int capacity);
常用的属性
属性 | 说明 |
---|---|
Count | 获取包含在其中的键值对的数目 |
IsReadOnly | 是否可读 |
Item | 获取或设置与指定键相关联的值 |
Keys | 获取包含Hashtable中的键的ICollection |
Values | 获取包含Hashtable中的值的ICollection |
添加元素的方法
public virtual void Add(object key,object value);
删除元素的方法
public virtual void Clear(); // 用于删除hashtable中的所有元素(清空hashtable)
public virtual void Remove(object key); // 用于删除指定键的元素
Hashtable的遍历
- 因为Hashtable的元素是键值对,所以需要用DictionaryEntry类型来遍历,这个类型表示一个键值对的集合:
- 注意hashtable中对元素的存储顺序和代码的顺序不一定相同 (栈?不是栈)
Hashtable ht = new Hashtable();
ht.Add("Name", "胡汉三");
ht.Add("Age", 17);
ht.Add("sex", "男");
foreach (DictionaryEntry DE in ht)
{
Console.WriteLine(DE.Key + "\t" + DE.Value);
}
Hashtable元素的查找
public virtual bool ContainsKey(object key); //判断是否包含指定键的元素
public virtual bool Contains(object key); //判断是否包含指定键的元素
public virtual bool ContainsValue(object value); // 判断是否包含指定值的元素
4 属性和方法
4.1 属性
- 属性不能作为 ref 或 out 参数传递
- 属性可以向程序中添加元数据(元数据是嵌入程序中的信息,如编译器指令或数据描述)
- 程序可以通过反射检查自己的元数据
- 通常使用属性与COM交互
- 有两种属性:公共语言运行库的基类中的属性 和 自定义的属性
使用公共语言运行库的基类中的属性 (如Serializable)
[System.Serializable]
public class MyClass{...}
// 该属性使得MyClass类中的成员可以序列化
使用自定义的属性
public class Stu
{
public string Name
{
get
{
return Name;
}
set
{
Name = value; // 使用系统自带的隐式参数value
}
}
}
4.2 方法
- 在面向对象中,当一个函数包含在类里时(身份是类的成员),它就成为了一个方法(成员函数)
- 方法在类或结构中声明
- C#中方法的定义和声明是在一起的(和C++不同)
- 方法名通常为动词或动词短语,所有单词首字母大写
- 方法的签名由这几个部分组成:方法名、形参的个数、修饰符和类型;返回类型和形参名称不是他的签名组成
- 一个方法的返回类型和它的形参列表中所引用的各个类型必须至少具有与该方法本身相同的可访问性
静态方法(Static)
- 静态方法是属于类的方法,不属于任何实例,任何实例不能访问到类的静态方法,必须通过类名来访问静态方法
- 在静态方法中不能使用 this 关键字
非静态方法
- 是针对实例的方法,
- 调用静态方法时可以使用this关键字
构造器
- 默认构造器(快捷键ctor)
- 构造器名字必须和类名相同
- 构造器无返回值类型,因为不必返回任何值,构造器只是为了分配内存
- 如果自己写构造器,一般声明为public,因为声明实例时,通常不是在这个类的里边,而是在这个类的外边声明这个类的实例
5 结构
- 结构是一种值类型,不是引用类型;在向方法传递结构时,结构是以值方式传递的而不是作为引用转递
- 结构的实例化可以不适用new关键字
- 结构可以声明构造函数,但它们必须带有参数
- 结构不能从一个结构或类继承,所有结构都直接继承自System.ValueType类
- 结构可以实现接口
- 在结构中,除非字段被声明为const或static,否则无法初始化
6 类
- 面向对象:抽象、封装、继承、多态等等
- 类是现实世界事物的模型
0 类和对象的关系
- 对象也叫实例,是类经过“实例化”后得到的内存中的实体
- 有些类是不能实例化的,如数学(Math Class),不能说“一个数学”
- 使用new操作符创建类的实例
静态成员和实例成员
- 静态(Static)成员在语义上标识它是“类的成员”
- 实例(非静态)成员在语义上标识它是“对象的成员”
6.1 常用的类修饰符
类修饰符 | 说明 |
---|---|
public | 不限制访问级别,都可以访问 |
new | 仅允许在嵌套类声明时使用 |
protected | 只能从其派生类进行访问(自己的实例都不能访问) |
internal | 只有其所在类可以访问 |
private | 只有在.NET中的应用程序或库才能访问 |
abstract | 抽象类,不允许创建类的实例 |
sealed | 密封类,不允许被继承 |
6.2 构造函数和析构函数
- 对象的生命周期以构造函数开始,以析构函数结束
- 当退出含有该对象的成员并且符合析构条时,析构函数会将自动释放这个对象占用的内存空间(.Net Framework的垃圾回收功能)
- 一个类中只有一个析构函数,并且无法主动调用析构函数,他是被自动调用的
public class Student
{
public Student(){...} // 构造函数
~Student(){...} // 析构函数
}
TODO: 构造函数和析构函数的访问级别?
6.3 继承
- C#中仅支持单继承,不支持多继承(不允许一次继承多个类,Object不算,所有类都继承自Object类),即使父类是抽象类也只能继承一个,但是可以实现多个基接口。
class Vehicle { }
class Toy { }
class Car : Vehicle,Toy { } // 报错
- 子类的访问级别不能超越父类,可以和父类的访问级别相同:
class Vehicle { } // 没有写默认就是internal
// public>internal 报错
public class Car : Vehicle { }
- protected访问修饰符修饰的成员,仅在继承链上的类可以访问这个成员。
- abstract修饰的抽象类不能被实例化,必须继承这个抽象类后再实例化。
- 子类的实例,从语义上来说也是基类的实例
// 验证子类的实例是否是父类的实例
Car car = new Car();
Console.WriteLine(car is Vehicle); // true
- 可以用一个父类类型的变量来引用一个子类类型的实例
// 一个父类类型的变量可以引用一个子类类型的实例
Vehicle vehicle = new Car(); // public class Car:Vehicle{...}
Object obj = new Vehicle();
Object obj2 = new Car();
- 派生类可以通过base.的方式调用父类的成员
public class FClass
{
public void ShowInfo()
{
Console.WriteLine("f.Show");
}
}
public class SClass:FClass
{
public new void ShowInfo()
{
Console.WriteLine("s.Show");
}
public void ShowFInfo()
{
base.ShowInfo(); // 显示调用父类的方法
}
}
6.4 多态(★★★)
- C#中多态的实现表现为:子类重载基类的虚方法或函数成员
- 父类的virtual和子类的override通常是成对使用的?(TODO)
- 当用一个父类类型的变量来接收子类类型的实例时,来调用一个被重写的成员,总是能够调到继承链上最新的版本.
class Vehicle
{
public virtual void Run()
{
Console.WriteLine("I'm Runing.");
}
}
class Car : Vehicle
{
public override void Run() // 重写了
{
Console.WriteLine("Car is running.");
}
}
class RaceCar : Car
{
// RaceCar中有两个Run的版本,一个是从Car继承过来的已经在Car中被重写过一次的Run版本,一个就是自己写的版本
public void Run() // 隐藏了从Car继承过来的Run
{
Console.WriteLine("Racecar is running.");
}
}
// 此时在主函数中这么写:
Vehicle v = new RaceCar();
v.Run(); // 调用的是Car的Run方法
// 如果想调用RaceCar自己的Run,写法如下:
RaceCar rc = new RaceCar();
rc.Run();
关于多态有意思的实验 1
public class Father
{
public int X { get; set; }
public int Y { get; set; }
public virtual int ADD()
{
return X + Y;
}
}
public class Son : Father
{
public int Z { get; set; }
public override int ADD()
{
X = Z;
return this.X + Y;
}
}
public class Program
{
static void Main(string[] args)
{
Father f = new Father();
f.X = 1;
Console.WriteLine("f.x = " + f.X); // 1
f.Y = 2;
Console.WriteLine("f.y = " + f.Y); // 2
int fAdd = f.ADD();
Console.WriteLine("f.Add = " + fAdd); // 3
Son s = new Son();
s.Z = 5;
Console.WriteLine("s.x = " + s.X); // 0
Console.WriteLine("s.Y = " + s.Y); // 0
int sAdd = s.ADD();
Console.WriteLine("s.Add = " + sAdd); // 5
Console.WriteLine();
}
}
不算是重写的情况
- 这样叫做子类成员对父类成员的隐藏
class Vehicle
{
public void Run()
{
Console.WriteLine("I'm Runing.");
}
}
class Car : Vehicle
{
// 这样不算构成override重写,是对父类的Run方法隐藏
// 在Car里边就有两个Run方法版本,一个是从父类继承的,一个是自己写的版本
public void Run()
{
Console.WriteLine("Car is Runing.");
}
}
// 如果此时在主函数中用一个父类类型的变量来接收子类类型的实例
Vehicle v2 = new Car();
v2.Run(); // 因为没有重写关系,结果就是调用Car中从父类继承过来的那个老版本的Run方法
关于多态的有意思的实验 2
public class StuA
{
private int x = 0;
private int y = 0;
public int X { get; set; }
public int Y { get; set; }
public virtual int Add()
{
return X + Y;
}
}
public class StuB : StuA
{
// 这样是对父类方法的重写,在StuB这个子类中,就只有这个Add()版本
public override int Add()
{
int x = 5;
int y = 7;
return x + y;
}
}
static void Main(string[] args)
{
StuB stuB = new StuB();
StuA stuA = (StuA)stuB;
stuA.X = 1;
stuA.Y = 2;
Console.WriteLine(stuB.Add()); // 12
Console.WriteLine(stuA.Add()); // 12 (调用的是子类的方法)
Console.WriteLine();
}
- 关键就在于子类有没有override父类的方法,没有override那子类就有两个版本的方法,override了就只有一个版本,就是自己重写的方法
TODO : 分析一下上边实验的成果?
- 想要更改父类的数据和行为有两种方法:1. 使用新的派生成员替换基类成员(new关键字);2. 重写父类的虚方法(override关键字),如上述例子可更换为:
new 关键字放在返回类型之前
public class StuA
{
private int x = 0;
private int y = 0;
public int X { get; set; }
public int Y { get; set; }
public virtual int Add()
{
return X + Y;
}
}
public class StuB : StuA
{
public new int Add()
{
int x = 5;
int y = 7;
return x + y;
}
}
-
类成员的访问级别是以类的访问级别为上限的。例如:类的访问级别是internal,那么类的成员的最高访问级别就是internal,即使是用public修饰了.
-
public 的访问级别最高,只要使用了名称空间的引用,在不同的项目级别里都是可以用public修饰的成员的。
-
private的访问级别最低,用private修饰的成员的访问级别会被限制在当前类体里,出了这个类就无法访问了,即使是在同一个项目级别里.
-
类的默认访问级别是internal,类里成员的默认访问级别是private.
-
父类的private成员也是被继承下来了的,除了实例构造器不被继承,所有的成员都会被继承.
-
protected修饰的成员会被限制在继承链上,如,当父类中有一个成员是被protected修饰的时候,他所有的子类都可以访问这个成员(甚至可以是跨程序集的),但是不在这个继承链上的类是无法访问到这个成员的. protected修饰符大多用于方法上,和internal进行组合.
-
使用父类类型变量来接收子类类型的实例(具体调用到哪个版本的函数是和实例的类型相关的,和变量的类型无关):
// 此时调用的Run()方法,是子类的Run方法,(创建的实例是哪个就调用的哪个的方法)
Vehicle v2 = new Car();
v2.Run();
- 重写或隐藏的发生条件:函数成员、可见、签名一致
- 属性和方法是可以被重写的
- virtual 不能和private、static、abstract、override同时使用
- override不能和new、static、virtual同时使用
- 重写方法只能用于重写基类中的虚方法
6.5 抽象类 & 抽象方法
-
solid设计原则:
1. Single Responsiblility Principle 单一职责原则 2. Open—Closed Principle 开放关闭原则 3. Liskov Substitution Principle 里氏替换原则 4. Interface Segregation Principle 接口隔离原则 5. Depedency Inversion Principle 依赖反转原则
-
抽象类:一旦一个类体里边有了一个抽象成员(abstract修饰的类),那么这个类就是抽象类,且这个时候这个抽象类也必须用abstract修饰。
-
抽象类是指存在函数成员没有被完全实现的类(这个函数成员不能有任何逻辑实现,连{}都不能有),因为那些没有被实现的成员函数要在那个抽象类的子类中实现,所以不能用private来修饰那个抽象成员函数,否则都无法在他的子类中实现,所以抽象类也不能够被密封
-
抽象方法也被叫做纯虚方法
-
开闭原则是指,应该把那些不变的稳定的成员进行封装,而将那些有可能发生改变的成员声明为抽象成员留给子类来实现
-
实现抽象方法的时候也要加上override关键字
-
抽象类就是专门为了做基类而生的(所以不能被密封,sealed修饰)
-
抽象类不能创建实例,但是可以声明它的变量
-
当一个类中的所有成员都是没有被实现的,都是抽象的,也即是一个纯抽象类,此时这样的类实际上就是接口(interface),此时只需把类的abstract修饰符转换成interface修饰符
-
声明抽象方法时,不能使用virtual、static、private修饰
6.6 密封类 & 密封方法
- 密封类不能作为基类被继承,但是它可以继承别的类或接口
- 密封类中不能声明受保护成员和虚成员,因为受保护的成员只能在从其派生类中进行访问,而虚成员只能在派生类中重写
- 密封类可以继承别的类或接口
——
- 对于密封方法,只能用于对基类的虚方法进行实现,其他的方法不能声明为密封方法
- 因此密封方法,sealed总是和override一起使用
关于密封方法和密封类的小实验1
public class ClassOne
{
public virtual void ShowInfo() { }
}
public class ClassTow : ClassOne
{
public int ID { get; set; }
public string Name { get; set; }
public override void ShowInfo()
{
Console.WriteLine("ID: " + ID + "\tName:" + Name);
}
}
关于密封方法和密封类的小实验2
- 一个程序,要求使用密封类封装用户名和密码,然后在主程序中通过密封类的对象对用户名和密码动态赋值输出:
public class User
{
public virtual string UserName { get; set; }
public virtual string Password { get; set; }
}
public sealed class ApUser:User
{
public sealed override string UserName { get => base.UserName; set => base.UserName = value; }
public sealed override string Password { get => base.Password; set => base.Password = value; }
}
public class Program
{
static void Main(string[] args)
{
ApUser au = new ApUser();
au.UserName = "牛犇";
au.Password = "123";
Console.WriteLine(au.UserName + ": " + au.Password);
Console.ReadLine();
}
}
7 接口
- 接口就是一种特殊的抽象类,里边的所有方法都是抽象的
- 通过接口可以实现多继承的功能
- 接口可以由方法、属性、事件、索引器或这四种成员类型的任何组合构成,但不能包含字段。
- 类和结构可以继承接口,而且可以继承多个接口
- 接口自身可以继承多个接口(直接用,分隔)
- 接口的产生:自底而上(重构)、自顶向下(设计)
- 接口中不能包含字段、构造函数、析构函数、静态成员或常量
- 接口中的成员必须是公共的
- 接口里边所有的方法要求都是public访问级别的,因此可以省略public修饰符,又因为接口代表的就是纯抽象类的含义,因此abstract也可以省略,也是要求abstract和public关键字在接口里边必须去掉:
interface VehicleBase
{
void Stop();
void Fill();
void Run();
}
- 因为去掉了abstract关键字,因此在实现时,实现的override关键字也要去掉,且要继承这个接口,就必须重写这个接口里的所有方法,如果不想把所有抽象方法都实现完,就恢复那个抽象方法的完整定义再写一遍:
// 因为没有完全实现接口,所以是一个抽象类
abstract class Vehicle:VehicleBase
{
// 要去掉实现的override关键字
public void Stop()
{
Console.WriteLine("Stopped.");
}
public void Fill()
{
Console.WriteLine("Pay and fill..");
}
// 保留一个不实现,留给以后的子类实现
abstract public void Run();
}
- 接口的命名规范,一般都是以大写字母 I 开头然后接一个名词,上边的接口VehicleBase所以应该改名为 IVehicle。
- 具体类->抽象类(其中的函数部分抽象,部分实现)->接口(函数全部抽象)
- 抽象类是未完全实现逻辑的类(可以有字段和非public成员,它们代表了“具体逻辑”).
- 抽象类的作用是代码复用,专门作为基类来使用,也具有解耦功能
- 把确定的封装起来,把不确定的开放,推迟到合适的子类中去实现。
- 接口是完全没有实现逻辑的“类”(也就是“纯虚类”;只有函数成员;成员全部public)
- 接口是为解耦而生:“高内聚,低耦合”,方便单元测试
- 接口和抽象类都不能被实例化,只能用来声明变量,引用具体类的实例(使用派生类的对象来实例化接口,或者叫使用父类类型的变量引用子类类型的实例)。
public class Program:ImyInterface
{
public string ID { get; set; }
public string Name { get; set; }
public void ShowInfo()
{
Console.WriteLine("ID: " + ID + ", Name: " + Name);
}
static void Main(string[] args)
{
Program program = new Program();
ImyInterface myInterface = program; // 相当于用父类类型的变量来接收一个子类类型的实例
myInterface.ID = "TM";
myInterface.Name = "牛犇";
myInterface.ShowInfo();
Console.ReadLine();
}
}
interface ImyInterface
{
string ID
{
get;
set;
}
string Name { get; set; }
void ShowInfo();
}
显示接口成员的实现
-
当两个接口用方法签名相同的成员时,而有一个类要同时实现这两个接口
-
通过接口名点的方式实现
-
显示接口成员中不能包含访问修饰符、abstract、virtual、override、static修饰符
情况1 , 不加以区分这两个方法,则在类中实现该成员则会导致两个接口都使用该成员作为它们的实现
interface ImyInterface1
{
int Add();
}
interface ImyInterface2
{
int Add();
}
class Calc : ImyInterface1, ImyInterface2
{
public int Add() // 既是接口1Add方法的实现,也是接口2Add方法的实现
{
throw new NotImplementedException();
}
}
情况2 , 如果需要两个接口成员实现不同的功能,则要显示地实现接口
interface ImyInterface1
{
int Add();
}
interface ImyInterface2
{
int Add();
}
class Calc : ImyInterface1, ImyInterface2
{
int ImyInterface1.Add()
{
int x = 1;
int y = 2;
return x + y;
}
int ImyInterface2.Add()
{
int x = 1;
int y = 2;
int z = 3;
return x + y + z;
}
}
- 显示接口成员是属于接口的成员不属于类的成员,因此无法通过类名点或者类的对象来访问这个显示接口成员,只能用接口对象来访问
static void Main(string[] args)
{
Calc calc = new Calc();
calc.Add(); // 这是错的,调用不到(这是属于接口的成员不是类的成员,也是根本不知道调哪个Add方法)
ImyInterface1 imyInterface1 = calc;
imyInterface1.Add(); // 这样才可以调到Add方法
Console.ReadLine();
}
8 异常处理语句
try…catch语句
- 在try{}语句块里放可能发生发生异常情况的程序代码
- catch{}语句块中放处理异常的程序代码
- catch语句后可以什么都不加,标识捕获所有类型的异常(?)
使用System.Exception通用类型捕获异常的实例
static void Main(string[] args)
{
try
{
object obj = null;
int N = (int)obj;
}
catch (Exception e)
{
Console.WriteLine("捕获异常:" + e);
}
Console.ReadLine();
}
捕获特定类型的实例
static void Main(string[] args)
{
try
{
checked
{
int x = 66666666;
int y = 66666666;
int z = x * y;
}
}
catch (OverflowException e)
{
Console.WriteLine("捕获异常:" + e);
}
Console.ReadLine();
}
结果:
try…catch…finally
- finally语句放在
try...catch
的最后边,无论程序是否会产生异常,最后都会执行finally语句区块中的程序代码,这些代码通常是为了使程序在任何情况下都必须执行的代码(如:使用了昂贵或有限的资源(如数据库连接或流),则无论发生异常,都应该尽快释放这些资源) - 实例:
static void Main(string[] args)
{
string str = "牛犇";
object obj = str;
try
{
int i = (int)obj;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
Console.WriteLine("程序执行完毕!");
}
Console.ReadLine();
}
结果:
throw语句
- throw用于在特定的情形下,主动引发一个异常。
- 语法为:
throw ExObject
- Exobject是指要抛出的异常对象,这个异常对象是派生自System.Exception类的类对象
- 通常将
throw
语句与try...catch
或try...finally
语句一起使用 - 当引发异常时,程序查找处理次异常的
catch
语句。 - 也可以使用
throw
语句重新引发已捕获的异常
static void Main(string[] args)
{
try
{
Test te = new Test();
int result = te.Div("2", "0");
// int result = te.Div("2", "a");
Console.WriteLine("DIV结果:" + result);
}
catch (FormatException fe)
{
Console.WriteLine("请输入数值格式数据");
}
Console.ReadLine();
}
public class Test
{
public int Div(string a,string b)
{
int x;
int y;
int num;
try
{
x = int.Parse(a);
y = int.Parse(b);
if (y==0)
{
// 当除数y为0时,主动抛出DivideByZeroException类的异常
throw new DivideByZeroException();
}
num = x / y;
return num;
}
catch (DivideByZeroException de)
{
Console.WriteLine("用0做除数发生异常!");
Console.WriteLine(de.Message);
return 0;
}
}
}
结果:
9 迭代器(TODO)
- 迭代器是可以返回相同类型的值的有序序列的一段代码,可以用作方法、运算符或get访问器的代码体
- 迭代器是遍历容器的对象,尤其是列表
- 迭代器可以用于
- 对集合中的每隔项执行操作
- 枚举自定义集合
- 扩展LINQ或其他库
- 创建数据管道、以便数据通过迭代器的方法在管道中有效流动
- 迭代器最重要的特性就是延迟计算,能够在需要结果的时候才开始计算
- 迭代器的返回类型必须为IEnumerable或IEumerator中的任意一种
10 分部类Partial类(TODO)
11 泛型
- 泛型就是一种可以是一个程序支持多种不同数据类型的技术
类型参数T
- 可以把T看作是一个占位符,它不是一种类型,但它代表了某种可能的数据类型,在使用时,类型参数T可以使用任何参数代替
- 一般将T作为描述性类型参数名的前缀
public interface ISessionChannel<TSession>
{
TSession Session {get; set;}
}
泛型接口
- 相比于一般的接口,泛型接口就是在声明时增加了一个< T >
interface 接口名<T>{...}