这四种成员都是用来表示数据的,所以放在一起讲。

成员 说明
常量 与类关联的常量值
字段 类的变量
方法 类可执行的计算和操作
属性 与读写类的命名属性相关联的操作
索引器 与以数组方式索引类的实例相关联的操作
事件 可由类生成的通知
运算符 类所支持的转换和表达式运算符
构造函数 初始化类的实例或类本身所需的操作
析构函数 在永久丢弃类的实例之前执行的操作
类型 类所声明的嵌套类型

字段

字段的定义

  • 字段(field) 是一种表示与对象或类型(类与结构体) 关联的变量
  • 字段是类型的成员,旧称“成员变量”
  • 与对象关联的字段亦称“实例字段”
  • 与类型关联的字段称为”静态字段”, 由static修饰

    1. class Program
    2. {
    3. static void Main(string[] args)
    4. {
    5. var stuList = new List<Student>();
    6. for (int i = 0; i < 100; i++)
    7. {
    8. var stu = new Student
    9. {
    10. Age = 24,
    11. Score = i,
    12. };
    13. stuList.Add(stu);
    14. }
    15. int totalAge=0;
    16. int totalScore=0;
    17. foreach (var stu in stuList) //计算平均年龄、分数
    18. //stu变量并不会与上下文冲突,此stu只在此括号内生效
    19. {
    20. totalAge += stu.Age;
    21. totalScore += stu.Score;
    22. }
    23. Student.AverageAge = totalAge / Student.Amout;
    24. Student.AverageScore = totalScore / Student.Amout;
    25. Student.ReportAmount();
    26. Student.ReportAverageAge();
    27. Student.ReportScore();
    28. }
    29. class Student
    30. {
    31. public int Age;
    32. public int Score; /字段
    33. public static int AverageAge;
    34. public static int AverageScore;
    35. public static int Amout;
    36. public Student()
    37. {
    38. Amout++;
    39. }
    40. public static void ReportAmount()
    41. {
    42. Console.WriteLine(Amout);
    43. }
    44. public static void ReportAverageAge()
    45. {
    46. Console.WriteLine(AverageAge);
    47. }
    48. public static void ReportScore()
    49. {
    50. Console.WriteLine(AverageScore);
    51. }
    52. }
    53. }

    字段的声明

  • 参见C#语言定义文档

  • 尽管字段声明带有分号,但它不是语句
  • ·字段的名字一定是名词

    1. class Student
    2. {
    3. public int Age;//字段会出现在类体之内
    4. public int Score;
    5. public static int AverageAge;
    6. public static int AverageScore;
    7. public static int Amout;
    8. public Student()
    9. {
    10. int sdadsa;//出现在函数体内的对象称为局部变量
    11. This.Age=0;
    12. //与 public int Age=0; 等价,但是在构造体内声明,赋值之前,字段会存在初始默认值
    13. }
    14. public static void ReportAmount()
    15. {
    16. Console.WriteLine(Amout);
    17. }
    18. public static void ReportAverageAge()
    19. {
    20. Console.WriteLine(AverageAge);
    21. }
    22. public static void ReportScore()
    23. {
    24. Console.WriteLine(AverageScore);
    25. }
    26. }
    27. }

    字段的初始值

  • 无显式初始化时,字段获得其类型的默认值,所以字段”永远都不会未被初始化

  • 实例字段初始化的时机——对象创建时
    • 声明实例字段时初始化值与在实例构造器里面初识化实例字段是一样的
  • ·静态字段初始化的时机——类型被加载(load) 时
    • 即静态构造器初始化时
    • 声明静态字段时设置初始化值与在静态构造器里面初始化静态字段其实是一样的

数据类型被运行环境加载时,它的静态构造器将会被调用,且只被调用一次。

  1. public static int Amout=100;
  2. static Student()
  3. {
  4. Amout = 200;
  5. }

只读字段

  • 实例只读字段

    1. class Program
    2. {
    3. static void Main(string[] args)
    4. {
    5. Student student1 = new Student(125);
    6. Console.WriteLine(student1.Id);
    7. student1.Id = 2; //无法对只读字段进行赋值操作
    8. }
    9. class Student
    10. {
    11. public readonly int Id; //称为只读字段,
    12. public Student(int ID)
    13. {
    14. this.Id = ID; //对Id添加非默认值的过程称为初始化
    15. }
    16. }
    17. }
  • 静态只读字段

只读字段为实例或类型保存一旦初始化后就不希望再改变的值。

属性

属性的定义

属性(property) 是一种用于访问对象或类型的特征的成员, 特征反映了状态
属性是字段的自然扩展

  • 从命名上看, field更偏向于实例对象在内存中的布局, property更偏向于反映现实世界对象的特征
  • 对外:暴露数据,数据可以是存储在字段里的,也可以是动态计算出来的
  • 对内:保护字段不被非法值”污染”

属性由Get/Set方法对进化而来

  • 使用 Get/Set 方法对之前: ```csharp class Program { static void Main(string[] args) { var stu1 = new Student() {

    1. Age = 20

    };

    var stu2 = new Student() {

    1. Age = 20

    };

    var stu3 = new Student() {

    1. // 非法值,污染字段
    2. Age = 200

    };

    var avgAge = (stu1.Age + stu2.Age + stu3.Age) / 3; Console.WriteLine(avgAge); } }

class Student { public int Age; }

  1. - 使用 Get/Set 后:
  2. ```csharp
  3. class Program
  4. {
  5. static void Main(string[] args)
  6. {
  7. try
  8. {
  9. var stu1 = new Student();
  10. stu1.SetAge(20);
  11. var stu2 = new Student();
  12. stu2.SetAge(20);
  13. var stu3 = new Student();
  14. stu3.SetAge(200);
  15. var avgAge = (stu1.GetAge() + stu2.GetAge() + stu3.GetAge()) / 3;
  16. Console.WriteLine(avgAge);
  17. }
  18. catch (Exception ex)
  19. {
  20. Console.WriteLine(ex.Message);
  21. }
  22. }
  23. }
  24. class Student
  25. {
  26. private int age;
  27. public int GetAge()
  28. {
  29. return age;
  30. }
  31. public void SetAge(int value)
  32. {
  33. if (value >= 0 && value <= 120)
  34. {
  35. age = value;
  36. }
  37. else
  38. {
  39. throw new Exception("Age value has error.");
  40. }
  41. }
  42. }

使用 Get/Set 来保护字段的方法至今仍在 C++、JAVA 里面流行(即 C++、JAVA 里面是没有属性的)。
因为 Get/Set 写起来冗长,微软应广大程序员请求,给 C# 引入了属性。
引入属性后:

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. try
  6. {
  7. var stu1 = new Student();
  8. stu1.Age=20;
  9. var stu2 = new Student();
  10. stu2.Age = 20;
  11. var stu3 = new Student();
  12. stu3.Age = 200;
  13. var avgAge = (stu1.Age + stu2.Age + stu3.Age) / 3;
  14. Console.WriteLine(avgAge);
  15. }
  16. catch (Exception ex)
  17. {
  18. Console.WriteLine(ex.Message);
  19. }
  20. }
  21. }
  22. class Student
  23. {
  24. private int age;
  25. public int Age
  26. {
  27. get
  28. {
  29. return age;
  30. }
  31. set
  32. {
  33. if (value >= 0 && value <= 120)
  34. {
  35. age = value;
  36. }
  37. else
  38. {
  39. throw new Exception("Age value has error.");
  40. }
  41. }
  42. }
  43. }

引入属性后代码简洁明了许多
又一个“语法糖”—属性背后的秘密
参照13141516表达式,语句讲解用 ildasm 反编译工具,反编译上面的示例。
发现编译器使用语法糖,把背后比较复杂的机制掩蔽了起来,即属性也是一种语法糖。
17字段、属性、索引器、常量 - 图1

17字段、属性、索引器、常量 - 图2

属性的声明

完整声明——后台(back) 成员变量与访问器(注意使用code snippet和refactor工具) :::info 属性(property) 是一种用于访问对象或类的特征的成员。属性的示例包括字符串的长度、字体的大小、窗口的标题、客户的名称,等等。
属性是字段的自然扩展,此两者都是具有关联类型的命名成员,而且访问字段和属性的语法是相同的。然而,与字段不同,属性不表示存储位置。相反,属性有访问器(accessor) , 这些访问器指定在它们的值被读取或写入时需执行的语句。因此属性提供了一种机制, 它把读取和写入对象的某些特性与一些操作关联起来;甚至,它们还可以对此类特性进行计算。
属性是使用property-declaration声明的:
property-declaration:
attributesopt property-modifiersopt type member-name {accessor-declarations}
property-modifiers:
property-modifier
property-modifiers property-modifier

::: 简略声明——只有访问器(查看IL代码)

动态计算值的属性

主动计算,每次获取 CanWork 时都计算,适用于 CanWork 属性使用频率低的情况。

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. try
  6. {
  7. var stu1 = new Student();
  8. stu1.Age = 12;
  9. Console.WriteLine(stu1.CanWork);
  10. }
  11. catch (Exception ex)
  12. {
  13. Console.WriteLine(ex.Message);
  14. }
  15. }
  16. }
  17. class Student
  18. {
  19. private int age;
  20. public int Age
  21. {
  22. get { return age; }
  23. set
  24. {
  25. age = value;
  26. }
  27. }
  28. public bool CanWork
  29. {
  30. get
  31. {
  32. return age > 16;
  33. }
  34. }
  35. }

被动计算,只在 Age 赋值时计算一次,适用于 Age 属性使用频率低,CanWork 使用频率高的情况。

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. try
  6. {
  7. var stu1 = new Student();
  8. stu1.Age = 12;
  9. Console.WriteLine(stu1.CanWork);
  10. }
  11. catch (Exception ex)
  12. {
  13. Console.WriteLine(ex.Message);
  14. }
  15. }
  16. }
  17. class Student
  18. {
  19. private int age;
  20. public int Age
  21. {
  22. get { return age; }
  23. set
  24. {
  25. age = value;
  26. CalculateCanWork();
  27. }
  28. }
  29. private bool canWork;
  30. public bool CanWork
  31. {
  32. get { return canWork; }
  33. }
  34. private void CalculateCanWork()
  35. {
  36. canWork = age > 16;
  37. }
  38. }

注意实例属性和静态属性
属性的名字一定是名词
只读属性——只有getter没有setter

  • 尽管语法上正确,几乎没有人使用”只写属性”,因为属性的主要目的是通过向外暴露数据而表示对象/类型的状态

    属性与字段的关系

    一般情况下,它们都用于表示实体(对象或类型)的状态
    属性大多数情况下是字段的包装器(wrapper)
    建议:永远使用属性(而不是字段) 来暴露数据, 即字段永远都是private或protected的
    建议:永远使用属性(而不是字段)来暴露数据,即字段永远是 private 或 protected 的。
    字段只在类内部使用,类之间交换数据,永远只用属性。

    索引器

    索引器的定义

    • 索引器(indexer)是这样一种成员:它使对象能够用与数组相同的方式(即使用下标)进行索引

      索引器的声明

    • 参加 C#语言定义文档

    • 注意:没有静态索引器

索引器一般都是用在集合上面,像如下示例这样用是很少见的(只是为了讲解方便)。

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. var stu = new Student();
  6. stu["Math"] = 90;
  7. stu["Math"] = 100;
  8. var mathScore = stu["Math"];
  9. Console.WriteLine(mathScore);
  10. }
  11. }
  12. class Student
  13. {
  14. private Dictionary<string, int> scoreDictionary = new Dictionary<string, int>();
  15. public int? this[string subject]
  16. {
  17. get
  18. {
  19. if (scoreDictionary.ContainsKey(subject))
  20. {
  21. return scoreDictionary[subject];
  22. }
  23. else
  24. {
  25. return null;
  26. }
  27. }
  28. set
  29. {
  30. if (value.HasValue == false)
  31. {
  32. throw new Exception("Score cannot be null");
  33. }
  34. if (scoreDictionary.ContainsKey(subject))
  35. {
  36. // 可空类型的 Value 属性才是其真实值。
  37. scoreDictionary[subject] = value.Value;
  38. }
  39. else
  40. {
  41. scoreDictionary.Add(subject, value.Value);
  42. }
  43. }
  44. }
  45. }

常量

什么是常量

  • 常量(constant)是表示常量值(即,可以在编译时计算的值)的类成员
  • 常量隶属于类型而不是对象,即没有“实例常量”
    • “实例常量”的角色由只读实例字段来担当
  • 注意区分成员常量与局部常量

    常量的声明

    各种“只读”的应用场景

  • 为了提高程序可读性和执行效率 —— 常量

  • 为了防止对象的值被改变 —— 只读字段
  • 向外暴露不允许修改的数据 —— 只读属性(静态或非静态),功能与常量有一些重叠
  • 当希望成为常量的值其类型不能被常量声明接受时(类/自定义结构体) —— 静态只读字段

17字段、属性、索引器、常量 - 图3

各种“只读”的应用场景

  • 常量:隶属于类型,没有所谓的实例常量
  • 只读字段:只有一次初始化机会,就是在声明它时初始化(等价于在构造函数中初始化)
  • 只读属性:对于类使用静态只读属性,对于实例使用实例只读属性
    • 要分清没有 Set,与 private Set 的区别
    • 常量比静态只读属性性能高,因为编译时,编译器将用常量的值代替常量标识符
  • 静态只读字段:字段没有类型局限,但常量只能是简单类型,不能是类/自定义结构体类型