C# – class, filed, property, const, readonly, get, set, init, required 使用基础
前言
心血来潮,这篇讲点基础的东西。
Field
比起 Property,Field 很不起眼,你若问 JavaScript,它甚至都没有 Field。
但在 C#,class 里头真正装 value 的其实是 Field,Property 只是 Filed 的读写器而已。
Field 长这样
public class Person { public int Age; }
使用
var person = new Person(); Console.WriteLine(person.Age); // int default is 0 person.Age = 10; // set value Console.WriteLine(person.Age); // get value: 10
const, readonly and init value
如果一个 Field 只能读,不能写,那我们可以用 const 去声明它。
public class Person { public const int Age = 5; public const int Age2; // Error: A const field requires a value to be provided } var person = new Person(); person.Age = 10; // Error : A readonly field cannot be assigned
const 的 init value 是写在 Filed 的结尾,一定要写 init value 哦,不然会报错。
readonly 也是用来声明只读 Field,它和 const 的不同在于,它比较宽松,在 constructor 阶段也允许 set init value。
public class Person { public readonly int Age = 1; public readonly int Age2; // no error public Person() { Age2 = 2; // if here no set init value Age2 will error. } }
如果两个都 set,construtor 会 override 掉 field assign。
注:下面这个写法是不 ok 的,它已经不算是 constructor 阶段的 set init value 了。
var person = new Person { Age = 15 // Error: A readonly field cannot be assigned };
Field 的日常(Dependancy Injection)
平时很少会使用 Field 的,绝大部分情况我们用 Property,除了 Dependancy Injection。
public class HomeModel : PageModel { private readonly ILogger _logger; public HomeModel(ILogger<HomeModel> logger) { _logger = logger; } public void OnGet() { _logger.LogInformation("Hello World"); } }
一个 readonly Field,通过 construtor set init value,然后使用。
C# 12.0 primary construtor 的写法
public class HomeModel(ILogger<HomeModel> logger) : PageModel { public void OnGet() { logger.LogInformation("Hello World"); } }
primary construtor 的 parameter 会被 compile 成 private Field,所以上面的代码和上一 part 的代码基本上是一样的。
唯一的不同是 primary construtor 目前无法声明 readonly Field。所以如果我们要 readonly 那就得多加一行。
public class HomeModel(ILogger<HomeModel> logger) : PageModel { private readonly ILogger _logger = logger; public void OnGet() { _logger.LogInformation("Hello World"); logger.LogInformation("Hello World"); // 注意:logger 依然是可用的,因为它就是一个 Field 啊 } }
Property
Property 是 Filed 的读写器,它长这样。
public class Person() { private int _age; public int Age { get { return _age; } set { _age = value; } } }
Filed 负责保存 value,Property 负责拦截读写过程。
这个是完整的写法,但日常生活中,大部分情况我们会用语法糖。
Auto Property
public class Person() { public int Age { get; set; } }
Auto Property 是一种语法糖写法,前面是 Field,后面配上一个 { get; set; } 表达。它 compile 后长这样。
compiler 会替我们拆开它们。最终任然是一个 Field、一个 get 方法、一个 set 方法。
readonly = no set
public class Person() { public int Age { get; } }
把 set 去掉就相等于 readonly Filed。注:没有 readonly Property 的,只有 no set。
set init value 的规则和 Field 一样。
public class Person { public int Age { get; } = 1; public Person() { Age = 2; } } var person = new Person { Age = 3 // Error: Property or indexer 'Person.Age' cannot be assigned to -- it is read only }
init keyword
上面例子中,实例化 Person 时,赋值是会报错的。这个是 readonly Field 的规则,我们只可以通过 construtor parameter 去实现 init 赋值。
这样局限太大了,于是 C# 6.0 推出了 init keyword。
public int Age { get; init; } = 1; var person = new Person { Age = 3 // no more error };
把 set 换成 init,它相等于 readonly 但是又允许实例化时赋值。
小总结:
有 3 个层次 assign value:field assign -> constructor assign -> new assign
它们分别对应 keyword:const -> readonly / no set -> init
required keyword
public class Person { public string Name { get; } // Warning Non-nullable property 'Name' must contain a non-null value }
这是一个 readonly Property,它有一个 warning,因为我没有声明 init value。
public class Person { public string Name { get; } = "init value"; public Person() { Name = "init value"; } }
我可以通过 2 种方式去设置 init value。这样就不会有 warning 了。
但是...如果它是 init keyword 呢?
public class Person { public string Name { get; init; } // Warning: Non-nullable property 'Name' must contain a non-null value }
init 允许我们在实例化时才给予 init value,也就是说 class 内是可以不需要声明 init value 的。
但这导致它又 Warning 了。为了解决这个问题,C# 11 推出了 required keyword。
public class Person { public required string Name { get; init; } }
声明 required keyword 后,它就不会 Warning 了。与此同时
var person = new Person(); // Error: Required member 'Person.Name' must be set
如果在实例化时忘记给予 init value,它还会报错提醒我们哦。
泛型限制也会报错哦
public class PersonOptions { public required string Name { get; set; } } public class Person<T> where T : new() // 泛型限制 T 必须允许无参数实例化 { } var person = new Person<PersonOptions>(); // Error : 'PersonOptions' cannot satisfy the 'new()' constraint
Best Practice
不限制模式
public class Person { public string Name { get; set; } = ""; public List<string> Values { get; set; } = []; public Person Child { get; set; } = null!; }
这个是最常见的定义,不做限制的好处是定义的时候省脑力,但对使用者来说就需要自己顾好好。
限制模式
public record class Person { public required string Name { get; init; } public required List<string> Values { get; init; } public required Person Child { get; init; } }
record + required + init 可以很大的限制对象的读写。
对使用者来说是好事,不会忘记赋值,也不会不小心修改到值。
总结
这篇介绍了 class, filed, property, const, readonly, get, set, init, required 的基本使用方式。
它之所以有点混乱,主要是因为 C# 是经过了许多版本才一点一点逐步推出这些特性的。
这些特性和 private, protected, public 类似,都是用于限制 class 的使用者,如果使用者乖乖的话,其实我们大可不必去写这些限制。