类和对象
基本类
修饰符
对于类或者方法,下列修饰符访问性通用
访问修饰符 | 同 类 | 同程序集子类 | 同程序集 非子类 | 不同程序集子类 | 不同程序集非子类 |
---|---|---|---|---|---|
public | √ | √ | √ | √ | √ |
internal | √ | √ | √ | ||
protected | √ | √ | √ | ||
private | √ | ||||
protected internal | √ | √ | √ | √ |
观察可得,protected只能用于子类继承,internal用于同程序集继承,public都可以继承,private都不能继承
构造函数
class People {
private String name;
private static int cnt;
// this调用该类本身所声明的其他构造函数
public People(): this("huro") {}
public People(String name) {
this.name = name;
}
// 静态构造函数都是私有的
// 静态构造函数是不可继承的
// 静态构造函数只能对静态数据成员初始化
// 如果没有静态构造函数,又有初始设定的静态字段,会自动生成一个静态构造函数
// 静态构造函数不允许出现访问修饰符例如 private
static People() {
cnt = 0;
}
}
class Student: People {
// base 调用该类父类的构造函数
public Student(String name): base(name);
}
构造函数的执行顺序
- 子类静态字段的直接初始化器
- 子类静态构造函数
- 子类实例字段的直接初始化器
- 父类静态字段的直接初始化器
- 父类静态构造函数
- 父类实例字段的直接初始化器
- 父类的构造函数
- 子类的构造函数
一个例子
public class Parent {
public static int a = initA(); // => 4 父类静态字段
public int b = initB(); // => 6 父类实例字段
public static int initA() {
Console.WriteLine("init A"); // => 4 父类静态字段的直接初始化器
return 1;
}
public static int initB() {
Console.WriteLine("init B"); // => 6 父类实例字段的直接初始化器
return 2;
}
static Parent() {
Console.WriteLine("running parent static"); // => 5 父类静态构造函数
}
public Parent() {
Console.WriteLine("running parent constructor"); // => 7 父类的构造函数
}
}
public class Child: Parent {
public static int x = initX(); // => 1 子类静态字段
public int y = initY(); // => 3 子类实例字段
public static int initX() {
Console.WriteLine("init X"); // => 1 子类静态字段的直接初始化器
return 3;
}
public static int initY() {
Console.WriteLine("init Y"); // => 3 子类实例字段的直接初始化器
return 4;
}
static Child() {
Console.WriteLine("running child static"); // => 2 子类静态构造函数
}
public Child() {
Console.WriteLine("running child constructor"); // => 8 子类的构造函数
}
}
public static void Main(String[] args) {
Child c = new Child();
}
结果为
init X
running child static
init Y
init A
running parent static
init B
running parent constructor
running child constructor
对象初始化
- 用大括号的形式初始化
public
属性
public class Bar {
public String Text {set; get};
}
// 初始化
Bar bar = new Bar { Text = "hello world!"};
- 用构造函数
属性
字段是指用 public
修饰符修饰的变量
属性是指封装的变量即用get
和 set
访问器封装的
传统的字段并不可以解决赋值的时候的判断问题,C#提供了一种方法来解决这个问题,可以在设置值的时候对值进行控制
主要分为
- 虚拟属性(用于继承)
- 只读属性
- 只写属性
- 重写属性
- 自动属性
class People {
protected string hobby;
public virtual string PHobby {
get { return ""; }
set {}
}
}
class Student: People {
private string stu_sex;
// set 内部的 value 是设置的值
// 例如 StuSex = 1; 那么value = 1
public string StuSex {
get {
return stu_sex;
}
set {
if (value.Equals("男") || value.Equals("女")) {
stu_sex = value;
} else {
Console.WriteLine("性别只可以是男或者女")
}
}
}
// 也可以定义只读属性
private string readonly_attr;
public string readonlyAttr {
get {
return readonly_attr;
}
}
// 同理也可以定义只写属性
// 静态只写属性
private static int cnt;
public static int StuCnt {
set {
if (cnt >= 0)
cnt = value;
}
}
// 重写属性
public override string PHobby {
get {return hobby;}
set {hobby = value;}
}
// 自动属性,系统默认给set和get函数
public string money {
get;
set;
}
}
需要注意
- 属性修饰符与方法修饰符相同,包括了
static
,virtual
,override
等 get
访问器返回类型与属性类型相同set
访问器没有返回值,但是有一个默认的隐式参数value
这个值的类型和属性类型相同- 访问器的访问性不能高于他所属的属性
- 不能把属性作为
ref
或out
参数传递
运算符重载
public class Square {
int width;
int height;
// 如果重载了 == 运算符 就也要重载 != 运算符
// 同理如果重载了 <= 也要重载 >=
// 定义了加法 + 不必定义减法 - 没有联系
public static bool operator == (Square s1, Square s2) {
return s1.width * s1.height == s2.width * s2.height;
}
public static bool operator != (Square s1, Square s2) {
return s1.width * s1.height != s2.width * s2.height;
}
}
public static void Main(String[] args) {
Square s1 = new Square { Width = 10, Height = 20 };
Square s2 = new Square { Width = 20, Height = 10};
Console.WriteLine(s1 == s2);
}
数据类型转换
- 隐式转换
public class Square {
public int Width { set; get; }
public int Height { set; get; }
// 隐式转换,则在转换的时候不需要明确指出转换对象
public static implicit operator int(Square s) {
return s.Width * s.Height;
}
}
public static void Main(String[] args) {
Square s1 = new Square { Width = 10, Height = 20 };
Console.WriteLine(s1);
}
- 显式转换
public class Square {
public int Width { set; get; }
public int Height { set; get; }
// 显式转换
public static explicit operator int(Square s) {
return s.Width * s.Height;
}
}
public static void Main(String[] args) {
Square s1 = new Square { Width = 10, Height = 20 };
// 需要明确指定转换为 int
Console.WriteLine((int)s1); // => 200
Console.WriteLine(s1); // => 命名空间.主类名+Square
}
分部类
// A1.cs
partial class A {
public void f1() {};
}
// A2.cs
partial class A {
public void f2() {};
}
// Main
A a = new A();
a.f1();
a.f2();
可以将一个类分布在不同的文件中,使用的时候编译器会自动将他们合起来。
常数
- 默认是
private
- 常量表达式的值是一个可以在编译的时候计算的值
- 不允许使用
static
运算符 - 和静态成员一样只能通过类名访问
class A {
const int a = 1;
}
结构
结构大家很熟了,就是用 struct
定义的
下面是区别
结构 | 类 |
---|---|
值类型 | 引用类型 |
可以不使用new实例化 | 必须使用new实例化 |
没有默认的构造函数但可以添加构造函数 | 有默认的构造函数 |
没有析构函数 | 有析构函数 |
接口
接口大家很熟了,就是用 interface
定义的
析构函数
方法 | 功能 |
---|---|
System.Object.Finalize | 这个方法用于回收对象的存储空间,析构函数是在回收对象的存储空间前调用的,最好不要手动调用 |
System.GC.Collect | 这个方法用于请求运行垃圾回收器,例如代码中有非常多的资源需要回收 |
Dispose | 这个方法用于立即销毁某个对象 |
Close | 某个资源可能以后会打开只是暂时关闭 |
大多数情况下是不需要写析构函数的,因为C#中有垃圾收集器
什么情况下需要自己写析构函数?
- 当该类中打开了一个文件,对象销毁时,应当关闭这个文件。
- 当该类中打开了数据库,对象销毁时,应当关闭这个连接。
- 当该类中申请了大量内存,对象销毁时,应当释放这些内存。
泛型类
.net 泛型 与 C++模板有所不同,C++在用特定类型实例化模板的时候是需要模板的源代码的,但是.net内部是针对基本类型即值类型(int, float ……)和引用类型(类类型)分别制作了类,所以不需要源代码了。
装箱和拆箱
如果在创建一个对象的时候没有指定好类型,可能会进行装拆箱工作,当发生失败的时候还会在运行的时候抛出异常。
var list = new LinkedList();
list.AddLast("2"); // 装箱
list.AddLast(3); // 装箱
foreach(string s in list1) {
// 拆箱
Console.WriteLine(s);
}
foreach循环中发现int无法转化为string类型,因此抛出异常
// System.InvalidCastException:“无法将类型为“System.Int32”的对象强制转换为类型“System.String”。”
指定了泛型后,在编译阶段就会给予错误提示,而且由于不用装箱和拆箱,性能更优
假定LinkedList
可以指定泛型类型
var list = new LinkedList<int>();
list.AddLast(1);
list.AddLast(3);
list.AddLast(5);
foreach (int i in list) {
Console.WriteLine(i);
}
用一个例子认识泛型
下面是一个链表
// 泛型类,泛型参数为 T
public class LinkedList<T> : IEnumerable<T>
{
// 自动属性,外界不可以修改,只可以内部修改
public LinkedListNode<T> First { get; private set; }
public LinkedListNode<T> Last { get; private set; }
// 增加元素
public LinkedListNode<T> AddLast(T node)
{
var newNode = new LinkedListNode<T>(node);
if (First == null)
{
First = newNode;
Last = First;
}
else
{
newNode.Prev = Last;
Last.Next = newNode;
Last = newNode;
}
return newNode;
}
// 实现遍历接口,c# 语法糖有 yield 相关说明
public IEnumerator<T> GetEnumerator()
{
LinkedListNode<T> current = First;
while (current != null)
{
yield return current.Value;
current = current.Next;
}
}
// 这里必须指定是 IEnumerable下的GetEnumerator 否则会和上面的那个方法出现歧义性,调用的时候编译器不知道调用哪一个
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
下面是链表节点的定义
public class LinkedListNode<T>
{
public LinkedListNode(T value)
{
this.Value = value;
}
// 只能内部设置
public T Value { get; private set; }
// internal set 是指同一个程序集可以修改,前面修饰符有说到了
public LinkedListNode<T> Next { get; internal set; }
public LinkedListNode<T> Prev { get; internal set; }
}
一些泛型的问题
- 无法假定T1一定有构造函数
class Demo<T1> {
private T1 innerObject;
public Demo() {
innerObject = new T1();
}
}
- 无法假定类型一定是引用或者是值类型
class Demo<T1, T2> {
public bool Compare(T1 op1, T2 op2) {
return op1 == op2;
}
}
由于无法假定类型,所以不知道赋值为 null
或者 0
,这个时候可以用 default
关键字进行赋值,如果T1
是引用类型,那么 default
会赋值为 null
class Demo<T1> {
public void empty(T1 op) {
op = default;
}
}
抗变和协变
抗变和协变其实很简单
public void Display(Shape o) {}
例如这个方法,我们可以传递 Shape
类,当然还可以传递他的子类,这个就叫做协变。
public Shape GetShape() {}
而作为返回值的时候,我们知道他返回的是 Shape
类,假设 Rectangle
类是 Shape
类的子类
Reactangle r = GetShape();
这行代码是不可以的,因为 Shape
不一定是 Rectangle
,这就叫做抗变,即抵抗改变,不愿意改变。
泛型接口的协变和抗变
如果泛型类型用out关键字标注,泛型接口就是协变的
public interface IIndex<out T> {
T this[int index] { get; }
int Count { get; }
}
协变类型参数只能作为方法的返回值或者get访问器返回值用。
如果泛型类型用in关键字标注,泛型接口则是抗变的
public interface IDisplay<in T> {
void Show(T item);
}
这乍一看好像和上面的抗变和协变反过来了。
其实是为了下面的使用
List<Object> list1 = new List<Object>(){0, 1, 2};
List<int> list2 = new List<int>();
foreach(Object obj in list1)
{
list2.Add((int)obj);
}
我们这样做转换的话,需要花费两倍的内存
IIndex<Rectangle> rectangles = RectangleCollection.GetRectangles();
IIndex<Shape> shapes = rectangles;
我们明确协变参数只会作为方法的返回值或get访问器
我们会尽可能复用 Rectangle
中的方法,因为返回 Rectangle
不也是 Shape
吗,这样就不会出现类型转换的问题。
同理抗变也是这样的。因此我们也可以解释了
- 如果实现了协变接口,那么可以完成子类向父类的转化
- 如果实现了抗变接口,那么可以完成父类向子类的转化
编译器会自动帮我们去实现复用。
类型约束
约束 | 说明 |
---|---|
where T: struct | T 必须是值类型 |
where T: class | T 必须是引用类型 |
where T: IFoo | T 必须实现接口或是接口本身 |
where T: Foo | T 必须是基类或者派生与基类 |
where T: new() | T 必须有一个公有无参构造函数 |
where T1: T2 | T1 必须 派生为 T2 |
注意如果用new() 作为约束,那么就必须是指定的最后一个约束
public class Demo<T> where T: Foo(), new() {}
泛型继承要求
必须是重复接口的泛型类型或者必须指定基类的类型
- 重复接口的泛型类型
public class Base<T> {
// ...
}
public class Derived<T>: Base<T> {
// ...
}
- 指定基类的类型
public class Base<T> {
// ...
}
public class Derived<T>: Base<string> {
// ...
}
派生类未必要是泛型类
public class Base<T> {
// ...
}
public class Derived: Base<int> {
//...
}
- 类型约束继承
原先 Farm
的要求是 T
必须是 Animal
类型的
class Farm<T>: where T: Animal {
// ...
}
继承后做的要求必须至少与基类型的相同,即至少范围要比Animal来的小,要是Animal的子类或者Animal
class SuperFarm<T>: Farm<T> where T: SuperCow {
// ...
}
以下就是不可以的,因为class是指引用类型,范围更广了
class SuperFarm<T>: Farm<T> where T: class {
// ...
}
静态成员
泛型类静态成员只能在类的一个实例中共享,即是不互通的
public class StaticDemo<T> {
public static int x;
}
StaticDemo<string>.x = 4;
StaticDemo<int>.x = 5;
泛型方法
void Swap<T>(ref T x,ref T y) {
T temp;
temp=x;
x=y;
y=temp;
}
这个方法用于交换两个数字的值
可以这样调用
int i = 4, j = 5;
Swap<int>(ref i, ref j);
也可以简化调用
Swap(ref i, ref j);
方法是泛型的可以简化调用,类不行。
泛型方法也可以重载
public T Bar<T>(T value) {
return value;
}
public int Bar(int value) {
return value;
}
public T Foo(T value) {
return Bar(value);
}
如果直接调用
Bar(1); // 会调用上述第二个,因为最为精确
Bar("1"); // 会调用上述第一个
Foo(1); // 会调用上述第三个和第一个,因为编译期间就决定了,这个Foo中的Bar是第一个,都是类型T
Foo("1"); //同上
泛型委托
public delegate T1 MyDelegate<T1, T2>(T2 op1, T2 op2) where T1: T2;