C# 类和对象

类和对象

基本类

修饰符

对于类或者方法,下列修饰符访问性通用

访问修饰符同 类同程序集子类同程序集 非子类不同程序集子类不同程序集非子类
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);
}
构造函数的执行顺序
  1. 子类静态字段的直接初始化器
  2. 子类静态构造函数
  3. 子类实例字段的直接初始化器
  4. 父类静态字段的直接初始化器
  5. 父类静态构造函数
  6. 父类实例字段的直接初始化器
  7. 父类的构造函数
  8. 子类的构造函数

一个例子

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    
对象初始化
  1. 用大括号的形式初始化 public 属性
public class Bar {
    public String Text {set; get};
}
// 初始化
Bar bar = new Bar { Text = "hello world!"};
  1. 用构造函数
属性

字段是指用 public 修饰符修饰的变量

属性是指封装的变量即用getset 访问器封装的

传统的字段并不可以解决赋值的时候的判断问题,C#提供了一种方法来解决这个问题,可以在设置值的时候对值进行控制

主要分为

  1. 虚拟属性(用于继承)
  2. 只读属性
  3. 只写属性
  4. 重写属性
  5. 自动属性
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;
    }
}

需要注意

  1. 属性修饰符与方法修饰符相同,包括了 static , virtual, override
  2. get 访问器返回类型与属性类型相同 set 访问器没有返回值,但是有一个默认的隐式参数 value 这个值的类型和属性类型相同
  3. 访问器的访问性不能高于他所属的属性
  4. 不能把属性作为 refout 参数传递
运算符重载
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);
} 
数据类型转换
  1. 隐式转换
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);
} 
  1. 显式转换
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#中有垃圾收集器

什么情况下需要自己写析构函数?
  1. 当该类中打开了一个文件,对象销毁时,应当关闭这个文件。
  2. 当该类中打开了数据库,对象销毁时,应当关闭这个连接。
  3. 当该类中申请了大量内存,对象销毁时,应当释放这些内存。

泛型类

.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; }
}
一些泛型的问题
  1. 无法假定T1一定有构造函数
class Demo<T1> {
    private T1 innerObject;
    public Demo() {
        innerObject = new T1();
    }
}
  1. 无法假定类型一定是引用或者是值类型
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 吗,这样就不会出现类型转换的问题。

同理抗变也是这样的。因此我们也可以解释了

  1. 如果实现了协变接口,那么可以完成子类向父类的转化
  2. 如果实现了抗变接口,那么可以完成父类向子类的转化

编译器会自动帮我们去实现复用。

类型约束
约束说明
where T: structT 必须是值类型
where T: classT 必须是引用类型
where T: IFooT 必须实现接口或是接口本身
where T: FooT 必须是基类或者派生与基类
where T: new()T 必须有一个公有无参构造函数
where T1: T2T1 必须 派生为 T2

注意如果用new() 作为约束,那么就必须是指定的最后一个约束

public class Demo<T> where T: Foo(), new() {}
泛型继承要求

必须是重复接口的泛型类型或者必须指定基类的类型

  1. 重复接口的泛型类型
public class Base<T> {
    // ...
}
public class Derived<T>: Base<T> {
    // ...
}
  1. 指定基类的类型
public class Base<T> {
    // ...
}
public class Derived<T>: Base<string> {
	// ...   
}

派生类未必要是泛型类

public class Base<T> {
    // ...
}
public class Derived: Base<int> {
 	//...   
}
  1. 类型约束继承

原先 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;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值