# 类成员

# 访问级别

在类定义中,字段,方法和属性都有自己的访问级别:

  • public:成员可以由任何代码访问。
  • private:成员只能由类中的代码访问,如果没有使用关键字,默认是这个级别。
  • internal:成员只能由定义它的程序集内部的代码访问。
  • protected:成员只能由类或派生类中的代码访问。
  • protected internal:成员能由定义它的程序集内部任意代码访问,在其他程序集中只能由它的派生类访问。

# 定义字段

class Person{
    public readonly int Age;
    public const int MaxAge = 35;

    public Person(int age)
    {
        Age = age;
    }
}

class Program
{
   static void Main(string[] strs)
   {
        Person p1 = new Person(20);
        Console.WriteLine(p1.Age);
        Console.WriteLine(Person.MaxAge);

   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

注意

  • readonly字段只能在定义时赋值或在构造函数中赋值。
  • const字段是静态的。

# 定义属性

属性的定义与字段类似,但要比字段复杂,它们在修改状态前执行一些操作。属性有get和set两个访问器,可以用于控制属性的读写,如果忽略其中一个,比如忽略get,那该属性就是只写。其格式如下:

[访问级别] [类型] 属性名
{
    get{
        //获取属性的代码
    }
    set{
        //设置属性的代码
    }
}

private int age;
public int Age{
    get{
        return age;
    }
    set{
        age=value;
    }
}

如果age字段是可以设置可空的,那么像如下这样设置:

private int? age;
public int? Age{
    get{
        return age;
    }
    set{
        age = value ?? 0;
    }
}

如果age字段只有在一定范围内才能赋值,可以像如下设置:

private int? age;
public int? Age{
    get{
        return age;
    }
    set{
        if(value >=0 && value <=30){
            age = value;
        }
        else
        {
            throw(new ArgumentOutOfRangeException("Prop Age", value, "Age input need to be in [0,30]!"));
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

属性可以使用virtual,override,abstract关键字就像方法一样,但是这几个关键字不能用于字段。访问器也可以有自己的访问级别,但是它的访问级别不可高于属性的访问级别,比如一个私有属性不能有公开的访问器。C#还提供了使用Lambda样式的属性:

int age=2;
public int Age => (age * 2);
1
2

Visual Studio可以使用快捷键将字段改成属性,在字段上右键-》快速操作和重构-》封装字段并使用属性,会变成如下:

int age=2;
public int Age { get => age; set => age = value; }
1
2

# 自动属性

属性是访问对象状态的首选方式,因为它们能控制外部代码访问对象内部的数据,为了简化操作,C#中有一种特殊的属性,称为自动属性,它以简化的语法声明属性,C#编译器会自动添加未键入的内容,也就是会声明一个存储属性的私有字段,并在属性的get和set中使用它。通常我们在编辑器中键入prop然后按两次Tab就会得到一个自动属性的模板。

public int MyProperty { get; set; }
public int Age{ get; } = 100;
1
2

使用自动属性时,只能通过属性访问数据,不能通过底层的私有字段来访问,因为我们不知道底层私有字段的名称,该名称是在编译期间定义的。注意自动属性要么都包含get和set访问器,要么只包含get访问器,但是可以改变访问器的访问权限。

# 定义方法

方法有几个关键字可描述:

  • virtual:方法可重写。
  • abstract:方法必须在非抽象的派生类中重写。
  • override:方法重写了一个基类的方法。
  • override sealed:在派生类中不能对这个方法做进一步的修改。
  • extern:方法定义在其他地方。

# 重写方法

当从基类继承的成员是virutal或abstract的方法,可以用override关键字重写这段代码。

class Animal
{
    public virtual void Run()
    {
        Console.WriteLine("animal run!");
    }
}

class Tiger : Animal
{
    public override void Run()
    {
        Console.WriteLine("tiger run!");
    }
}
class Program
{
    static void Main(string[] args)
    {
        Animal tiger = new Tiger();
        tiger.Run();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 覆盖方法

覆盖方法可以覆盖基类中的虚拟的和非虚拟的方法,也可以不使用关键词new,但是编辑器会有警告,因为这可能是无意导致的操作,一般如果是有意重写方法的话最好还是使用new关键字声明。

class Animal
{
    public void Run()
    {
        Console.WriteLine("animal run!");
    }
    public virtual void Eat()
    {
        Console.WriteLine("animal eat!");
    }
}

class Tiger : Animal
{
    public new void Run()
    {
        Console.WriteLine("tiger run!");
    }
    public new void Eat()
    {
        Console.WriteLine("tiger eat!");
    }
}
class Program
{
    static void Main(string[] args)
    {
        Tiger tiger = new Tiger();
        tiger.Run();
        tiger.Eat();
        Animal animal = tiger;
        animal.Run();
        animal.Eat();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 重写和覆盖的区别

从上面的运行结果可以看出,如果使用基类引用来调用子类实例的方法,重写会调用子类的方法,覆盖会调用基类的方法。

# 基类和当前类

有两个关键词代表基类和当前类,它们分别是base和this。this引用的是当前的对象实例,base则代表引用的基类。这两个关键字都不能用于静态方法。

this有个常用的用法就是限定当前类的成员。

# 元组析构

前面介绍了元组,在函数返回一个元组时,接收结果必须要解析下才行,如果在类中使用元组,那么可以使用元组析构(tuple deconstruction)来避免被解析的过程:

class Position
{
    public double axisX { get; }
    public double axisY { get; }
    public Position(double x, double y) => (axisX, axisY) = (x, y);
    public void Deconstruct(out double x, out double y) => (x, y) = (axisX, axisY);
}
class Program
{
    static void Main(string[] args)
    {
        Position p1 = new Position(20, 30);
        (double x, double y) = p1;
        Console.WriteLine($"axis x:{x},axis y:{y}");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

元组析构需要给类添加Deconstruct()函数即可。

# 嵌套类

我们可以在类中定义类,这样修饰嵌套类的不仅仅是public和internal,还可以是private和protected。通过嵌套类可以修改其包含类的私有字段。看下例:

class Animal
{
    private string name = "BaoBao";
    public string Name => name;
    public void Run()
    {
        Console.WriteLine("animal run!");
    }

    public class Holer
    {
        public void SetName(Animal anim,string str)
        {
            anim.name = str;
        }
        public void Run()
        {
            Console.WriteLine("Holder let animal run!");
        }

    }
}
class Program
{
    static void Main(string[] args)
    {
        Animal animal = new Animal();
        Console.WriteLine(animal.Name);
        Animal.Holer holder = new Animal.Holer();
        holder.SetName(animal, "little cat!");
        Console.WriteLine(animal.Name);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 部分类和方法

如果一个类包含很多成员时,比较容易引起混乱,代码文件也比较长。此时有一种方法就是可以使用region将代码分隔开,比如:

public class TestClass{
    #region Fields
    private int IntVal;
    #endregion

    #region Methods
    public void GetVoid(){
        return IntVal;
    }
    #endregion
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12

还有一种方法是使用部分类定义(partial class definition),就是将类的定义放在多个文件中,比如将字段,属性和构造函数放在一个文件中,将方法放在另一个文件中,只需在类的定义前面加限定词partial。同理,也可以定义部分方法(partial method)。部分方法在一个部分类中定义,在另一个部分类中实现,定义方法时也需要使用partial关键字。

public class Person
{
    public int age;
}
public interface IRun
{
    void PersonRun();
}
public interface IEat
{
    void PersonEat();
}
public partial class Student:Person,IRun
{
    public string name = "zhangsan";

    public void PersonRun()
    {
        Console.WriteLine("Student run!");
    }

    partial void SetName(string str);
}
public partial class Student:IEat
{
    public void PersonEat()
    {
        Console.WriteLine("Student eat!");
        SetName("Eater");
    }

    partial void SetName(string str)
    {
        name = str;
    }
}
class Program
{
    static void Main()
    {
        Student st1=new Student();
        st1.PersonEat();
        Console.WriteLine(st1.name);
        st1.PersonRun();
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

注意:

  • 部分类分开实现的接口相当于整类实现的全部接口。
  • 基类在多个定义文件中指定时必须是同一个基类,因为C#中类只能继承一个基类。
  • 部分方法可以是静态的,但它们总是私有的,且不能有返回值。
  • 部分方法不能使用out参数,但可以使用ref参数,部分方法也不能使用virtual,abstract,override,new,sealed或extern修饰符修饰。
  • 部分方法如果没有实现,编译代码时编译器会完全删除该方法,还会删除该方法的所有调用,这样如果不实现部分方法,就不会影响性能。

# 接口

# 接口的实现

其格式如下:

interface [interfaceName]
{

}
1
2
3
4

接口和类有几点不同:

  • 不允许使用访问修饰符(private),所有接口成员默认都是公共的。
  • 接口成员中的方法不能包含代码体,不能定义字段成员,类型成员。
  • 不能用关键字static,virtual,abstract或sealed来定义接口成员。
  • 接口也能用new覆盖基接口的成员。

具体实现看下面实例:

interface IMoveBiology
{
    void Run();
    void Eat();
}

class Animal
{
    public void Run()
    {
        Console.WriteLine("Any animal all run in this way!");
    }
}

class Tiger : Animal,IMoveBiology
{
    void IMoveBiology.Eat()
    {
        Console.WriteLine("Tiger is Eatting!");
    }
}
class Cat : IMoveBiology
{
    public void Run()
    {
        Console.WriteLine("Cat is running!");
    }

    public void Eat()
    {
        Console.WriteLine("Cat is eatting!");
    }
}
class Program
{
    static void Main(string[] args)
    {
        Tiger tiger = new Tiger();
        tiger.Run();

        Console.WriteLine("-------------");

        Cat cat = new Cat();
        cat.Run();
        cat.Eat();

        Console.WriteLine("-------------");

        IMoveBiology tigerI = new Tiger();
        tigerI.Run();
        tigerI.Eat();

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

注意:

  • 实现接口的类必须实现接口所有成员。可以使用virtual或abstract来实现接口成员。
  • 继承一个实现接口的基类意味着隐式支持这个接口。
  • 显式地实现接口就是在实现接口时成员以接口名限定。显式地实现接口只能以接口引用调用接口方法。

来看下在接口中定义属性的情况:

interface IMoveBiology
{
    string Name
    {
        set;
    }
}

class Animal : IMoveBiology
{
    private string name;
    //string IMoveBiology.Name { set => name = value; get => name; }
    public string Name { set => name = value; get => name; }
}


class Program
{
    static void Main(string[] args)
    {
        Animal anim = new Animal();
        anim.Name = "baobao";
        Console.WriteLine(anim.Name);
        IMoveBiology biologyI = anim;
        Console.WriteLine(anim.Name);

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

关于接口中的属性应注意:

  • 在接口中定义的属性可以省略get或set,形式很像类的自动属性,但实际不是。
  • 如果是隐式实现接口,在实现类中可以添加没有的get或set访问器,但是如果是显式实现接口,那么就不能添加没有的访问器。