C# 类

类是一个对象的蓝图,包含对象的数据和能够进行的操作。类之与对象,就如人类和具体的某一个人。

类的定义

语法

[类可见性修饰符] [类修饰符] class 类名
{

}

定义一个类名为Person的类

public class Person{

}

类的实例化

使用new关键字创建类的实例。

语法

类型名称 变量名称 = new 类型名称(参数);

实例化类

Person zhangsan = new Person();

通过new来创建对象,调用了类的构造函数。

类的成员

类的成员包括常量、字段、方法、属性、索引器、事件和嵌套类型。

属性、索引器、事件本质也是字段和方法,可以理解为主要成员类型时字段和方法,前者保存了对象的状态,后者定义了对象的行为。

成员可见性修饰符

成员修饰符

静态成员和实例成员

一个类可以有静态成员和非静态成员。

静态成员使用static修饰符,和类型相关,通过类名.成员进行访问。静态成员不能访问实例成员。

实例成员不使用static修饰符,和类型的每一个实例相关,通过变量名.成员进行访问。实例成员可以访问静态成员。

常量

常量是不变的值.

语法

[可见性修饰符] const 类型 常量名 = 值

常量的值必须是字面量。

字段

类的相关数据可以通过字段来保存。

语法

[可见性修饰符] [static] [readonly] 类型 变量名[= 值];

字段默认可见性修饰符是private

字段变量如果不初始化,则使用默认值.

值可以是常量,构造函数调用,静态方法调用。

定义和使用字段

public class Person{
    public string name;
}

Person p = new Person();
p.name = "zhang san";
Console.WriteLine(p.name);

只读字段

只能在构造函数中赋值,之后不能修改.

方法

方法是一个类型可以进行的操作

方法的定义

语法

[可见性修饰符] [方法修饰符] 返回类型 类型名称(参数类型 参数名称,...){

}

定义和使用方法

class Person{
    public string name;

    public void PrintName(){
        Console.Write(this.name);
    }
}

Person p = new Person();
p.name = "zhang san";
p.PrintName();

在成员方法内部,this.成员访问自身的成员.如果成员名称没有歧义,可以忽略this.

方法重载

同一个方法名可以定义不同的方法,只要参数类型和数量是不同的。

class Person{
    public string name;

    public void PrintName(){
        Console.Write(name);
    }
     public void PrintName(int times){
        for(int i=0;i<times;i++){
            Console.Write(name);
        }
    }
}

Console类的WriteLine方法定义了非常多的重载,用于输出不同类型。

重载歧义

使用重载的时候,有可能有多个方法都匹配签名,这是就需要显示指定类型。

static void Func(string s)
{

}

static void Func(int? id)
{

}

Func((string)null);

构造函数

通过new实例化类的时候,调用的是类的构造函数。

在定义类时,会有一个没有参数的构造参数,称作默认构造函数。我们也可以为其自行定义构造函数。

语法

[可见性修饰符] 类型名称(参数类型 参数名称,...)

构造函数没有返回类型。 如果自行定义构造函数,那么系统就不会为我们再定义默认构造函数,如果还需要,可以再自行定义无参构造函数。

构造函数在CLR中的名称是.ctor.

可以在一个构造函数中通过this调用另一个构造函数

    public class Person
    {
        public string name;

        public Person() : this("no name")
        {

        }

        public Person(string name)
        {
            this.name = name;
        }
    }

静态构造函数

类支持静态构造函数,静态构造函数会在第一次使用类之前被调用。 语法

static 类型名称(){

}

属性

属性提供对对象数据的读写方法.

属性的定义

语法

[可见性修饰符] [属性修饰符] 属性类型 属性名
{
    get
    {
        return 值;
    }
    set
    {
        设置语句
    }
}

其中 get 部分需要返回值,set部分可以对传入的值进行操作.

通常,get返回的是一个字段,set是为字段赋值.

定义属性

  public class Person
    {
        private string name;

        public string Name
        {
            get
            {
                return name;
            }
            set
            {
                name = value;
            }
        }
  }

在内部,属性是两个方法.

 public class Person
    {
        private string name;

        public string get_Name()
        {
                return name;
        }
         public void set_Name(string value){
             name = value;
         }
  }

始终记得,属性是方法.

属性的使用

使用属性

Person p = new Person();
p.Name = "zhang san";
Console.WriteLine(p.Name);

可以看到,属性的使用方式和字段一致。

对象初始化

Person p = new Person
{
    Name = "zhang san"
};

可以在构造函数后添加多个属性的初始化语句简化对象的创建,其中属性不能重复。
如果使用默认的构造函数,可以省略圆括号。
属性的作用

使用字段已经可以保存对象的状态,但它不能很好的解决权限问题。

如果需要外界只能读,不能写,字段做不到。

如果需要对写入做验证,字段做不到。

属性是为了对操作进行验证,保证对象的状态不被外界任意改变。同时,和访问字段有同样的语法。

自动属性

由于大多数情况下,属性都是简单的对字段进行包装,为了简化语法,可以使用自动属性.

语法

[可见性修饰符] 类型名称 属性名称 { [可见性修饰符]get; [可见性修饰符]set; }

可以为get和set中的一个设置不同的可见性修饰符.

定义外部只读的属性

public string Name {get;private set;}
只读属性和只写属性

只有get的方法,被称作只读属性;对于使用者,如果set不可访问,也相当于只读属性.

只有set的方法,被称作只写属性;对于使用者,如果get不可访问,也相当于只写属性.

只写属性很少见.

静态属性

一般静态属性都是只读的,用来表达某项设置.比如Environment.OSVersion获得系统版本.

索引器

索引器允许对象如数组一样进行访问。从功能上看,像是重载了[]运算符。

语法

[可见性修饰符] [成员修饰符] 类型 this[参数列表]{
    get
    {
        //读取
    }
    set
    {
        //写入
    }
}

使用索引器

 public class Indexable
    {
        int[] array;

        public Indexable(Int32[] array)
        {
            this.array=array??throw new ArgumentNullException(nameof(array));
        }
        public int Count
        {
            get
            {
                return array.Length;
            }
        }
        public virtual int this[int index]
        {
            get
            {
                return array[index];
            }
            set
            {
                array[index]=value;
            }
        }
    }

索引器是可以重载的,一个类可以定义多个索引器.

在内部,索引器是带参数的属性,名称是get_Itemset_Item.

终结器

终结器是对象进行垃圾回收时调用的方法.

终结器的定义

语法

~类型名称(){

}

终结器没有返回值

终结器没有可见性修饰符

终结器没有参数

终结器实际是对object类型的Finalize方法的重写

终结器的使用

只有在使用非托管资源时,才需要使用终结器。配合终结器的同时,需要实现IDisposable接口

Dispose模式

 public class Disposable : IDisposable
    {
        private bool disposed = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (disposing)
                {
                    //释放托管状态(托管对象)。
                }
                //释放非托管对象
                disposed = true;
            }
        }

        ~Disposable()
        {
            //释放非托管对象
            Dispose(false);
        }

        public void Dispose()
        {
            //释放托管对象和非托管对象
            Dispose(true);
            //阻止调用终结器
            GC.SuppressFinalize(this);
        }
    }

使用非托管资源时,始终使用using包裹的,不要依靠终结器。 终结器是忘记using的最后一道释放非托管资源的保证,但其影响垃圾回收效率,而且执行时机不确定。

事件

事件是对象能够发布的通知,包含注册和注销方法,使用者可以订阅和解除订阅事件.

事件的定义
[可见性修饰符] event 委托类型 事件名称;

通常,事件的委托类型是EventHandler类型或者Eventhandler<T>类型,包含一个事件的发送者sender,和事件参数e。后者要求一个继承自EventArgs类型的泛型参数。 事件的委托类型通常没有返回值。

触发事件

var handler = 事件名称;
if(handler != null){
    handler(this, e);
}

当没有任何注册时,事件是null. 之所以要用一个临时变量来保存事件,是由于事件的注册/注销可能来自多个线程。

C#6.0后的简便语法

{
    事件名称?.Invoke(this, e);
}

带事件的类

    public class Button
    {
        public event EventHandler Clicked;

        public void Click()
        {
            Clicked?.Invoke(this, EventArgs.Empty);
        }
    }

事件内部,是一个注册和一个注销方法,以及一个委托字段。

事件的内部

 public class Button
    {
        EvenetHandler clicked;

        public void add_Clicked(EventHandler handler){
            clicked = Delegate.Combile(clicked,handler);
        }

        public void remove_Clicked(EventHandler handler){
            clicked = Delegate.Remove(clicked,handler);
        }

        public void Click()
        {
            clicked?.Invoke(this, EventArgs.Empty);
        }
    }

上述代码省略了线程同步部分,仅展示结构。

事件的使用

语法

订阅事件

变量.事件名称 += 委托;

注销事件

变量.事件名称 -= 委托;

订阅/注销事件

Button btn = new Button();

var handler = new EventHandler(delegate
{
    Console.WriteLine("button is clicked.");
});

btn.Clicked+=handler;

btn.Click();

btn.Clicked-=handler;

btn.Click();
自定义事件注册注销

可以自己定义事件的注册和注销方法

[可见性修饰符] 委托类型 事件名称
{
    add
    {
        注册事件
    }
    remove
    {
        注销事件
    }
}

自定义事件注册注销

 public class Button
{
    public void Click()
    {
        clicked?.Invoke(this, EventArgs.Empty);
    }

    EventHandler clicked;

    public event EventHandler Clicked
    {
        add
        {
            clicked = (EventHandler)Delegate.Combine(clicked, value);
        }
        remove
        {
            clicked = (EventHandler)Delegate.Remove(clicked, value);
        }
    }

}

运算符重载

运算符重载使类型可以支持不同的操作符.

语法

public static 类型 operator 运算符(类型1 变量名1[,类型2 变量名2])
{
}

类型必须是自身。

如果操作符时一元操作符,类型1必须是类型自身。

如果时二元操作符,类型1或类型2必须有一个是自身。

定义并使用运算符重载

 public class Point
{
    public Point() : this(0, 0)
    {

    }
    public Point(int x, int y)
    {
        X=x;
        Y=y;
    }

    public int X { get; private set; }
    public int Y { get; private set; }

    public static Point operator +(Point p1, Point p2)
    {
        return new Point(p1.X+p2.X, p1.Y+p2.Y);
    }
}

Point p1 = new Point(1, 2);
Point p2 = new Point(3, 4);
Point p3 = p1 + p2;
Console.WriteLine(p3.X + "," + p3.Y);

运算符重载的内部,是静态方法.比如二元+op_Addtion.

并非所有的.NET语言都支持运算符重载,建议同时提供对应的静态方法.

支持重载的运算符

+,-,!,~,++,--

+,-,*,/,%重载后支持+=,-=,*=,/=,%=

|,&,^重载后支持|=,&=,^=

==,!=两者需要同时实现

<,>两者需要同时实现

>=,<=两者需要同时实现

类型转换重载

类型转换重载使类型可以隐式或显式转换成另一个类型

语法

public static [implicit|explicit] operator 类型1(类型2 变量名)
{
}

类型1和类型2必须有一个是自身类型。

定义和使用类型转换重载

class Person
{
    string name;

    public Person(string name){
        this.name = name;
    }

    public static explicit operator string(Person p)
    {
        return p.name;
    }
}

Person p = new Person("zhang san");
stirng s = (string)p;

隐式类型转换内部方法名称是op_Implicit

显式类型转换内部方法名称是op_Explicit

并不是所有.NET语言都支持类型转换重载.

嵌套类型

类的内部也可以定义其他类型,如接口、委托、类、枚举和结构.

嵌套类型可以访问外部类的所有成员而不受可见性修饰符的限制。

定义和使用嵌套类型

public class Outer
{
    public class Inner
    {
    }
}

Outer.Inner obj = new Outer.Inner();

访问私有成员

public class Outer
{
    private int value;

    public Outer(int value){
        this.value = value;
    }

    public class Inner
    {
        Outer outer;

        public Inner(Outer outer){
            this.outer = outer;
        }

        public void Print(){
            Console.WriteLine(value);
        }
    }
}

Outer outer = new Outer(200);
Outer.Inner inner = new Outer.Inner(outer);
inner.Print();

通常,嵌套类型不是public,而是会实现接口,通过接口给使用者。

类的继承

继承可以使得子类获得父类的所有功能.

子类又被称作派生类

父类又被称作基类

语法

[可见性修饰符] [类修饰符] class 类名: 基类名称
{
}

如果一个类型没有声明父类,默认继承自object

只能继承自一个类,不支持多继承.

所有类型都直接或间接继承自object

object类的成员


//初始化 System.Object 类的新实例。
public Object();

~Object();

//对两个对象进行相等性比较,都为null返回true
public static Boolean Equals(Object objA, Object objB);

//对两个对象进行引用相等性比较,,都为null返回true
public static Boolean ReferenceEquals(Object objA, Object objB);

//判断对象是否和另一个对象相等,默认进行引用比较.
public virtual Boolean Equals(Object obj);

//获取一个对象的哈希码.相等的对象的哈希码因相同.
public virtual Int32 GetHashCode();

//获取当前对象的 System.Type。反射使用.
public Type GetType();

//返回表示当前对象的字符串表示,默认是类型全名.
public virtual String ToString();

//创建当前对象的浅复制
protected Object MemberwiseClone();

两个静态成员Equals,ReferenceEquals相等性相关.

两个实例成员Equals,GetHashCode和相等性相关

一个成员GetType用于反射.

一个成员ToString用于输出对象信息

一个保护成员MemberwiseClone进行复制

构造函数的继承

构造函数不支持继承,如果父类有多个构造函数,子类可以定义相同的,并通过base调用父类的构造函数。

调用父类的构造函数

class Person
{
    string name;

    public Person(string name)
    {
        this.name = name;
    }
}

class Employee: Person
{
    public Employee(string name) :base(name){

    }
}

Employee employee = new Employee("zhang san");

在创建子类对象时,会优先调用父类构造函数.

方法重写

父类的virtual,abstract修饰的方法,子类可以通过override重新实现.

在重写中可以通过base访问基类的成员.

 public class Animal
    {
        public virtual void Bark()
        {
             Console.WriteLine("叫");
        }
    }

    public class Dog : Animal
    {
        public override void Bark()
        {
            Console.WriteLine("汪");
        }
    }

调用

Animal animal = new Dog();
annimal.Bark();

输出:汪

使用虚函数,调用的时候不用知道具体的类型,调用的是子类的方法。

这种运行时决定调用哪个方法的行为,被称作多态。多态使得调用者只管用,而不用管实现。

属性,索引器,事件也是方法,也支持重写。

阻止方法重写

子类可以通过sealed阻止其子类再次重写.

 public class Animal
    {
        public virtual void Bark()
        {
             Console.WriteLine("叫");
        }
    }

    public class Dog : Animal
    {
        public sealed override void Bark()
        {
            Console.WriteLine("汪");
        }
    }
    public class LittleDog : Dog
    {
        //这里不能重写Bark
    }

成员覆盖

子类使用new修饰成员,表示子类的成员和父类的成员无关.

 public class Animal
 {
     public virtual void Bark()
     {
         Console.WriteLine("叫");
     }
 }

public class Dog : Animal
{
    public new void Bark()
    {
        Console.WriteLine("汪");
    }
}
Animal animal = new Dog();
animal.Bark();

输出:叫.调用的Animal类的Bark.

Dog dog = new Dog();
dog.Bark();

输出:汪.调用的Dog类的Bark.

new割裂了父类和子类成员的关系,他们是各自类型的个方法,只有使用自身类型被调用,不支持多态。

静态成员的访问

静态成员不参与继承体系,也不支持重写等操作.

父类的静态成员,也可以通过子类名.成员名访问.

抽象类

使用abstract修饰的类是抽象类。抽象类可以包含没有具体实现的abstract方法。

抽象类不能被实例化。

继承自抽象类的非抽象类必须实现所有abstract方法。

抽象类的继承

public abstract class Base
{
    public abstract void F();
}

public abstract class Base2 : Base
{

}

public class Drived : Base2
{
    public override void F()
    {
        Console.WriteLine("Drived");
    }
}

静态类

使用static修饰的类是静态类.

静态类只能包含静态成员.

静态类不能被实例化.

静态类不能被继承.

静态类通常用于辅助类.

辅助方法

static class Util
{
    void Swap<T>(ref T a,ref T b)
    {
        T t = a;
        a = b;
        b = t;
    }
}

扩展方法

扩展方法可以为一个类型提供如同成员方法的访问方式。

public static class StringExtensions
{
    public static string Left(this string s,int length)
    {
        return s.Substring(0,length);
    }
}

扩展方法需要定义在顶级静态类中。

扩展方法的第一个参数前使用this修饰。

编译时,会为扩展方法所在的方法名,类型名,程序集加上ExtensionAttribute特征。

定义扩展方法时,尽量不要定义太高层的类型,比如object,以免污染代码提示。

如果扩展方法的第一个参数不能为null,抛出ArguemntNullException,而不是NullReferenceException,表明是扩展方法。