记录
本章前面提到,记录是支持值语义的引用类型。这种类型可以减少你自己需要编写的代码,因为编译器会实现按值比较记录的代码,并提供其他一些特性
不可变类型
记录的一种主要用例是创建不可变类型(不过使用记录也可以创建可变类型)。不可变类型只包含类型状态不能改变的成员。可以使用构造函数或者对象初始化器初始化这种类型,但之后就不能再改变任何值。
名义记录
可以创建两种类型的记录:名义记录和位置记录。名义记录看起来与类相同,只不过使用record关键字代替了class关键字,如类型Book1所示。在这里,使用了只能初始化的设置访问器,禁止在创建实例后改变其状态
public record Book1
{public string Title {get;set;} = string.Empty;public string Publisher {get;set;} = string.Empty;
}
可以在记录中添加本章介绍的构造函数和其他所有成员。编译器会创建一个使用记录语法的类。记录与类的区别在于,编译器会在记录中创建另外一些功能。编译器会重写基类object的GetHashCode()和ToString()方法,创建方法和运算符重载来比较不同的值的相等性,创建方法来克隆现有对象以及创建新对象,此时可以使用对象初始化器修改一些属性的值
位置记录
实现记录的第二种方式是使用位置记录语法。这种语法在记录名称的后面使用圆括号指定成员。这种语法称为“主构造函数”。
public record Book2(string Title, string Publisher);
使用花括号可以在record中添加需要的东西,重载的构造函数,方法,或前面章节介绍的成员
public record Book2(string Title, string Publisher)
{// add your members, overloads
}
对于位置记录,编译器会创建与名义记录相同的成员,并且会添加解构方法(元组中的对自定义类型的解构)。
记录的相等比较
类对于相等性比较的默认实现是比较引用。创建相同类型的两个新对象后,即使把它们实现为相同的值,它们也是不同的,因为它们引用了堆上的不同对象。
记录具有不同的行为,记录对于相等性比较的实现是,如果两个记录的属性值相同,那么它们就相等。
// See https://aka.ms/new-console-template for more informationA a = new("张三", 15);
A a2 = new("张三", 15);
Console.WriteLine(a == a2);// True
Console.WriteLine(object.ReferenceEquals(a,a2));// FalseA a3 = new("张三", 15);
B b = new("张三", 15);
Console.WriteLine(a3 == b);//错误(活动) CS0019 运算符“==”无法应用于“A”和“B”类型的操作数
Console.WriteLine(object.ReferenceEquals(a3, b));// Falserecord A(string name, int age);
record B(string name, int age);
结构
前面看到,类和记录为在程序中封装对象提供了一种出色的方式。它们被存储到堆上,让数据的生存期变得更加灵活,但性能上稍微有些损失。存储在堆上的对象需要垃圾收集器做一些工作,以便释放不再需要的对象占用的内存。为了减少垃圾收集器需要做的工作,可以为较小的对象使用栈
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()方法
public readonly struct Dimensions
{public double Diagonal => Math.Sqrt(Length * length + Width * width);
}
- 结构采用前面讨论过的按值传递语义,即值会被复制。结构与类和记录还有其他区别:
- 结构不支持继承。可以使用结构实现接口,但不能继承另外一个结构
- 结构总是有一个默认的构造函数。对于类,如果定义了构造函数,则不会再生成默认构造函数。结构类型与类不同。结构总是有一个默认的构造函数,你无法创建一个自定义的无参构造函数。
- 对于结构,可以指定字段在内存中如何布局。(见13章)
- 结构存储在栈上,或者如果结构是堆上存储的另外一个对象的一部分,就会内联存储它们。当把结构用作对象时,如把它们传递给一个对象参数,或者调用了一个基于对象的方法,就会发生装箱,值也会被存储到堆上
枚举类型
枚举是一个值类型,包含一组命名的常量
public enum Color
{Red = 0,Blue = 1,Green = 2
}
可以声明枚举的变量,如c1
void ColorSamples()
{Color c1 = Color.Red;Console.WriteLine(c1);
}
//运行结果
/*
Red
*/
默认情况下,enum的类型是int。这个基本类型可以改为其他整数类型(byte,short,int,带符号的long和无符号的long)。命名常量的值从0开始递增,但它们可以改为其他值
public enum Color : short
{Red = 1,Blue = 2,Green = 3
}
以下是使用枚举的顶级语句代码
// 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
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
string str1 = "hello";
Console.WriteLine(str1);
UpdateStringTest(ref str1);
Console.WriteLine(str1);//hello2void UpdateStringTest(ref string str)
{str = "hello2";
}
如以下java代码所示,若传递对象引用给另一个方法,并在该引用上创建新对象,这样的操作将不会影响到原有的声明
在c#中 不使用ref关键字,那么c#和java的这种行为是一致的,不同的是,c#中可以通过使用ref关键字修饰对象参数
也就是说c#中对使用了ref引用的对象参数引用创建新对象,原有的对象引用会指向另一个方法中新创建的对象
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示例
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修饰符
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
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
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;
}
调用者需要决定是应该复制返回的值,还是应该使用引用
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()方法的调用者在收到结果后改变该方法的输入
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,因为收到的原始值将被复制
ref readonly SomeValue bigger4 = ref MaxReadonly(in one, in two);
SomeValue bigger5 = ref MaxReadonly(in one, in two);
out参数
如果方法应该返回多个值,那么有不同的选项可以用采用。一种选项是创建一个自定义类型,另一种选项是为参数使用ref关键字。使用ref关键字时,需要在调用方法前先初始化参数。数据将被传入方法,并从方法返回。如果方法只应该返回数据,可以使用out关键字。
/*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#语法中集成了元组
声明和初始化元组
// 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;}
}
元组解构
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();
元组的返回
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类型(这是一个结构)并复制值
// 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*/
// 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参数中
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判断是否为空
int? i = null; //bool b = i.HasValue;
Console.WriteLine(i is null); //True
Console.WriteLine(i is not null); //False
分部类型
partial关键字可用于class struct interface前
// 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();
}