记录
本章前面提到,记录是支持值语义的引用类型。这种类型可以减少你自己需要编写的代码,因为编译器会实现按值比较记录的代码,并提供其他一些特性
不可变类型
记录的一种主要用例是创建不可变类型(不过使用记录也可以创建可变类型)。不可变类型只包含类型状态不能改变的成员。可以使用构造函数或者对象初始化器初始化这种类型,但之后就不能再改变任何值。
名义记录
可以创建两种类型的记录:名义记录和位置记录。名义记录看起来与类相同,只不过使用record关键字代替了class关键字,如类型Book1所示。在这里,使用了只能初始化的设置访问器,禁止在创建实例后改变其状态
c#
public record Book1
{
public string Title {get;set;} = string.Empty;
public string Publisher {get;set;} = string.Empty;
}
可以在记录中添加本章介绍的构造函数和其他所有成员。编译器会创建一个使用记录语法的类。记录与类的区别在于,编译器会在记录中创建另外一些功能。编译器会重写基类object的GetHashCode()和ToString()方法,创建方法和运算符重载来比较不同的值的相等性,创建方法来克隆现有对象以及创建新对象,此时可以使用对象初始化器修改一些属性的值
位置记录
实现记录的第二种方式是使用位置记录语法。这种语法在记录名称的后面使用圆括号指定成员。这种语法称为"主构造函数"。
c#
public record Book2(string Title, string Publisher);
使用花括号可以在record中添加需要的东西,重载的构造函数,方法,或前面章节介绍的成员
c#
public record Book2(string Title, string Publisher)
{
// add your members, overloads
}
对于位置记录,编译器会创建与名义记录相同的成员,并且会添加解构方法(元组中的对自定义类型的解构)。
记录的相等比较
类对于相等性比较的默认实现是比较引用。创建相同类型的两个新对象后,即使把它们实现为相同的值,它们也是不同的,因为它们引用了堆上的不同对象。
记录具有不同的行为,记录对于相等性比较的实现是,如果两个记录的属性值相同,那么它们就相等。
c#
// See https://aka.ms/new-console-template for more information
A a = new("张三", 15);
A a2 = new("张三", 15);
Console.WriteLine(a == a2);// True
Console.WriteLine(object.ReferenceEquals(a,a2));// False
A a3 = new("张三", 15);
B b = new("张三", 15);
Console.WriteLine(a3 == b);//错误(活动) CS0019 运算符"=="无法应用于"A"和"B"类型的操作数
Console.WriteLine(object.ReferenceEquals(a3, b));// False
record A(string name, int age);
record B(string name, int age);
结构
前面看到,类和记录为在程序中封装对象提供了一种出色的方式。它们被存储到堆上,让数据的生存期变得更加灵活,但性能上稍微有些损失。存储在堆上的对象需要垃圾收集器做一些工作,以便释放不再需要的对象占用的内存。为了减少垃圾收集器需要做的工作,可以为较小的对象使用栈
c#
public readonly struct Dimensions
{
public Dimensions(double length, double width)
{
Length = length;
Width = width;
}
public double Length{get;}
public double Width{get;}
}
定义结构的成员与定义类和记录的成员的方式相同。前面已经看到了Dimensions结构的构造函数。下面的代码演示了为Dimensions结构体添加一个Diagonal属性
,它调用了Math类的Sqrt()方法
c#
public readonly struct Dimensions
{
public double Diagonal => Math.Sqrt(Length * length + Width * width);
}
- 结构采用前面讨论过的按值传递语义,即值会被复制。结构与类和记录还有其他区别:
- 结构不支持继承。可以使用结构实现接口,但不能继承另外一个结构
- 结构总是有一个默认的构造函数。对于类,如果定义了构造函数,则不会再生成默认构造函数。结构类型与类不同。结构总是有一个默认的构造函数,你无法创建一个自定义的无参构造函数。
- 对于结构,可以指定字段在内存中如何布局。(见13章)
- 结构存储在栈上,或者如果结构是堆上存储的另外一个对象的一部分,就会内联存储它们。当把结构用作对象时,如把它们传递给一个对象参数,或者调用了一个基于对象的方法,就会发生装箱,值也会被存储到堆上
枚举类型
枚举是一个值类型,包含一组命名的常量
c#
public enum Color
{
Red = 0,
Blue = 1,
Green = 2
}
可以声明枚举的变量,如c1
c#
void ColorSamples()
{
Color c1 = Color.Red;
Console.WriteLine(c1);
}
//运行结果
/*
Red
*/
默认情况下,enum的类型是int。这个基本类型可以改为其他整数类型(byte,short,int,带符号的long和无符号的long)。命名常量的值从0开始递增,但它们可以改为其他值
c#
public enum Color : short
{
Red = 1,
Blue = 2,
Green = 3
}
以下是使用枚举的顶级语句代码
c#
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
DoSomething("Red");
DoSomething("Green");
DoSomething("Blue");
//GetNames方法返回一个包含枚举中的所有名称的字符串数组
foreach (string s in Enum.GetNames(typeof(Color)))
{
Console.WriteLine(s);
}
//从枚举中返回所有值
foreach (int s in Enum.GetValues(typeof(Color)))
{
Console.WriteLine(s);
}
void DoSomething(string color)
{
//使用字符串和Enum.TryParse()来获得相应的Color的值
bool b = Enum.TryParse<Color>(color, out Color color1);
if (b)
{
switch (color1)
{
case Color.Red:
Console.WriteLine(Color.Red);
break;
case Color.Blue:
Console.WriteLine(Color.Blue);
break;
case Color.Green:
Console.WriteLine(Color.Green);
break;
default:
break;
}
}
}
public enum Color
{
Red = 0,
Blue = 1,
Green = 2
}
ref、in和out
值类型是按值传递的,所以当把一个变量赋值给另外一个变量时,列如将变量传递给方法时,将复制该变量的值。
有一种方法可以避免这种复制。如果使用ref关键字,将按引用类型传递值类型
ref
c#
int a = 1;
ChangeAValueType(ref a);
Console.WriteLine($"the value of changed to {a}");//输出后 a = 2;
void ChangeAValueType(ref int x)
{
x = 2;
}
对于不可变的string类型也可以使用ref
c#
string str1 = "hello";
Console.WriteLine(str1);
UpdateStringTest(ref str1);
Console.WriteLine(str1);//hello2
void UpdateStringTest(ref string str)
{
str = "hello2";
}
如以下java代码所示,若传递对象引用给另一个方法,并在该引用上创建新对象,这样的操作将不会影响到原有的声明
在c#中 不使用ref关键字,那么c#和java的这种行为是一致的,不同的是,c#中可以通过使用ref关键字修饰对象参数
也就是说c#中对使用了ref引用的对象参数引用创建新对象,原有的对象引用会指向另一个方法中新创建的对象
java
public class Main {
public static void main(String[] args) {
Test test = new Test();
test.setName(1);
createNewTest(test);
System.out.println("Main = " + test.getName());
}
static void createNewTest(Test test){
test = new Test(2);
System.out.println("createNewTest = " + test.getName());
}
}
class Test{
public int name;
public Test() {
}
public Test(int name) {
this.name = name;
}
public int getName() {
return name;
}
public void setName(int name) {
this.name = name;
}
}
//运行结果
/*
Connected to the target VM, address: '127.0.0.1:54981', transport: 'socket'
createNewTest = 2
Main = 1
Disconnected from the target VM, address: '127.0.0.1:54981', transport: 'socket'
*/
c#对自定义类使用ref示例
c#
SomeData someData = new() { Value = 1};
Console.WriteLine($"调用Update前{someData}");
UpdateSomeData(ref someData);
Console.WriteLine($"调用Update后{someData}");
void UpdateSomeData(ref SomeData someData)
{
someData = new SomeData(2);
}
class SomeData
{
public int Value { get; set; }
public SomeData()
{
}
public SomeData(int value)
{
Value = value;
}
public override string? ToString()
{
return $"Value : {Value}";
}
}
/*运行结果
调用Update前Value : 1
调用Update后Value : 2
*/
in
如果在向方法传递一个值类型时,想要避免复制值的开销,但又不想在方法内改变值,就可以使用in修饰符
c#
void PassValueByReferenceReadonly(in SomeValue data)
{
//data.Value1 = 4;//错误(活动) CS8332 无法分配给 变量"data"的成员,或将其用作 ref 分配的右侧,因为它是只读变量
}
struct SomeValue
{
public SomeValue(int value1, int value2, int value3, int value4)
{
Value1 = value1;
Value2 = value2;
Value3 = value3;
Value4 = value4;
}
public int Value1 { get; set; }
public int Value2 { get; set; }
public int Value3 { get; set; }
public int Value4 { get; set; }
}
ref return
为了避免方法在返回时复制值,可以在声明返回类型时添加ref关键字,并在返回值时使用return ref
c#
ref SomeValue Max(ref SomeValue x, ref SomeValue y)
{
int sumx = x.Value1 + x.Value2 + x.Value3 + x.Value4;
int sumy = y.Value1 + y.Value2 + y.Value3 + y.Value4;
if (sumx > sumy)
{
return ref x;
}
else
{
return ref y;
}
}
可以使用一个条件表达式来替换if/else语句,此时需要在表达式中使用ref关键字来比较sumx和sumy,根据比较的结果,将把ref x或者 ref y
写入一个局部值的ref,然后返回该局部值的ref
c#
ref SomeValue Max(ref SomeValue x, ref SomeValue y)
{
int sumx = x.Value1 + x.Value2 + x.Value3 + x.Value4;
int sumy = y.Value1 + y.Value2 + y.Value3 + y.Value4;
ref SomeValue result = ref (sumx > sumy) ? ref sumx : ref sumy;
return ref result;
}
调用者需要决定是应该复制返回的值,还是应该使用引用
c#
SomeValue one = new SomeValue(1,2,3,4);
SomeValue two = new SomeValue(1,2,3,4);
//将结果复制到了bigger1变量中,尽管该方法被声明为返回ref
SomeValue bigger1 = Max(ref one, ref two);
//使用ref 关键字来调用方法,得到一个ref return
ref SomeValue bigger2 = Max(ref one, ref two);
//这里使用readonly,只是为了指定bigger3变量不会被改变,如果设置属性来修改它的值,编译器将会报错
ref readonly SomeValue bigger3 = Max(ref one, ref two);
Max()方法不会修改它的任何输入。这就允许为参数使用in关键字,如MaxReadonly()方法所示,但是这里必须把返回类型的声明改为ref readonly。如果不这么做,将允许MaxReadonly()方法的调用者在收到结果后改变该方法的输入
c#
ref readonly SomeValue MaxReadonly(in SomeValue x, in SomeValue y)
{
int sumx = x.Value1 + x.Value2 + x.Value3 + x.Value4;
int sumy = y.Value1 + y.Value2 + y.Value3 + y.Value4;
ref SomeValue result = ref (sumx > sumy) ? ref sumx : ref sumy;
return ref result;
}
现在调用者必须把结果写入一个ref readonly变量,或者将结果赋值到一个新的局部变量中。对于bigger5,不需要使用readonly,因为收到的原始值将被复制
c#
ref readonly SomeValue bigger4 = ref MaxReadonly(in one, in two);
SomeValue bigger5 = ref MaxReadonly(in one, in two);
out参数
如果方法应该返回多个值,那么有不同的选项可以用采用。一种选项是创建一个自定义类型,另一种选项是为参数使用ref关键字。使用ref关键字时,需要在调用方法前先初始化参数。数据将被传入方法,并从方法返回。如果方法只应该返回数据,可以使用out关键字。
c#
/*
int.Parse()方法期望收到一个string,如果解析成功,它会返回一个int。如果不能将string解析为int,将抛出一个异常。为了避免这种异常,可以使用int.TryParse()方法。无论解析是否成功,这个方法都返回一个布尔值。解析操作的结果通过一个out参数返回。
*/
// bool TryParse(string? s, out Int32 result);
/*
要调用TryParse()方法,可以使用out修饰符传递一个int。使用out修饰符时,不需要在调用TyrParse()方法前声明或者初始化该变量
*/
Console.Write("Please enter a number: ");
string? input = Console.ReadLine();
if (int.TryParse(input, out int x))
{
Console.WriteLine();
Console.WriteLine($"read an int:{x}");
}
元组
元组允许把多个对象组合为一个对象,但又没有创建自定义类型的复杂性
c#7开始,c#语法中集成了元组
声明和初始化元组
c#
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
void IntroTuples()
{
(string AString, int Number, Book book) tuple1 = ("magic", 42, new("Professional c#", "Wrox Press"));
Console.WriteLine($"a string:{tuple1.AString},number:{tuple1.Number},book:{tuple1.book}");
/*
在把元组字面值赋值给元组变量的时候,也可以不声明其成员,此时,可以使用ValueTuple结构,
的成员名称Item1,Item2和Item3来访问元组的成员
*/
var tuple2 = ("magic", 42, new Book("Professional c#", "Wrox Press"), "", "", "", "", "", "", "", "11");
Console.WriteLine($"a string:{tuple2.Item1},number:" +
$"{tuple2.Item2},book:{tuple2.Item11}");
/*
在字面值中,可以为元组字段分配名称,这需要首先定义一个名称,其后跟上一个冒号,也就是
与对象字面值相同的写法
*/
var tuple3 = (AString: "magic", Number: 42, Book: new Book("Professional c#", "Wrox Press"));
Console.WriteLine($"a string:{tuple3.AString},number:" +
$"{tuple3.Number},book:{tuple3.Book}");
//类型匹配的时候,可以把一个元组赋值给另一个元组
(string a, int b, Book c) tuple4 = tuple3;
Console.WriteLine($"a string:{tuple4.a},number:{tuple4.b},book:{tuple4.c}");
/*
元组的名称也可以从源推断出来,对于变量tuple5,第二个成员是一个字符串,其值为一本书的名称
代码中没有为这个成员分配名称,但因该属性的名称为Title,所以将自动使用Title作为元组的名称
*/
Book book = new Book("Professional c#", "Wrox Press");
var tuple5 = (Number:42,book.Title);
Console.WriteLine($"Number:{tuple5.Number},book:{tuple5.Title}");
}
IntroTuples();
class Book
{
public String Title { get; set; }
public String Publisher { get; set; }
public Book(String Title, String Publisher)
{
this.Title = Title;
this.Publisher = Publisher;
}
}
元组解构
c#
void TupleDeconstruction()
{
var tuple1 = (AString: "magic", Number: 42, Book: new Book("Professional c#", "Wrox Press"));
(string AString, int Number, Book book) = tuple1;
Console.WriteLine($"a string:{AString},number:{Number},book:{book}");
//如果不需要某些变量,可以使用discard,discard是名称为_的c#占位符变量。它们用来忽略结果
(_, _, Book book1) = tuple1;
Console.WriteLine($"book:{book1.Title}");
}
TupleDeconstruction();
元组的返回
c#
static (int result, int remainder) Divide(int dividend, int divisor)
{
int result = dividend / divisor;
int remainder = dividend % divisor;
return (result, remainder);
}
static void ReturningTuples()
{
(int result, int remainder) = Divide(7, 2);
Console.WriteLine($"7 / 2 - result: {result}, remainder: {remainder}");
}
ReturningTuples();
元组的值传递
元组的引用传递是值传递,原因在于,在为元组使用c#语法时,编译器在后台会使用ValueTuple类型(这是一个结构)并复制值
c#
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
(string a, int b, short c)= ("A", 1, 2);
(string a1, int b2, short cd) = (a, b, c);
a1 = "B";
Console.WriteLine("a="+a);
Console.WriteLine("a1=" + a1);
/*
* 输出
Hello, World!
a=A
a1=B
*/
c#
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
(string a, int b, short c) a= ("A", 1, 2);
(string a, int b, short c) b = a;
b.a = "B";
b.b = 5;
b.c = 6;
Console.WriteLine("元组b的a="+b.a);
Console.WriteLine("元组a的a=" + a.a);
/*
* 输出
元组b的a=B
元组a的a=A
*/
对自定义类型的解构
为完成自定义类型的解构,只需要创建Deconstruct()方法(也被称为解构器),将分离的部分放入out参数中
c#
Person person = new("first", "last", 42);
(string firstName, string lastName, int age) = person;
Console.WriteLine($"firstName:{firstName},lastName:{lastName},age:{age}");
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public Person()
{
}
public Person(string firstName, string lastName, int age)
{
FirstName = firstName;
LastName = lastName;
Age = age;
}
public void Deconstruct(out string firstName, out string lastName, out int age)
{
firstName = FirstName;
lastName = LastName;
age = Age;
}
}
模式匹配
使用is null 和is not null判断是否为空
c#
int? i = null; //bool b = i.HasValue;
Console.WriteLine(i is null); //True
Console.WriteLine(i is not null); //False
分部类型
partial关键字可用于class struct interface前
c#
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
SampleClass s = new ();
s.MethodOne();
s.MethodTwo();
//当编译包含这两个源文件的同时,会创建一个SampleClass类,它有两个方法MethodTwo(),MethodOne()
//所有的特性,XML注释,接口,泛型参数特性和成员会合并
//SampleClassAutoGenerated.cs
using System;
partial class SampleClass
{
public void MethodTwo()
{
Console.WriteLine("MethodTwo方法");
}
//如果不返回void就必须在另一个分部类里提供实现
public partial void APartialMethod()
{
Console.WriteLine("APartialMethod方法");
}
}
//SampleClass.cs
using System;
partial class SampleClass
{
public void MethodOne()
{
Console.WriteLine("MethodOne方法");
//如果另一个分部类没有提供实现,则编译器会忽略该调用
APartialMethod();
}
public partial void APartialMethod();
}