记录,结构,枚举,ref,in和out 元组

记录

本章前面提到,记录是支持值语义的引用类型。这种类型可以减少你自己需要编写的代码,因为编译器会实现按值比较记录的代码,并提供其他一些特性

不可变类型

记录的一种主要用例是创建不可变类型(不过使用记录也可以创建可变类型)。不可变类型只包含类型状态不能改变的成员。可以使用构造函数或者对象初始化器初始化这种类型,但之后就不能再改变任何值。

名义记录

可以创建两种类型的记录:名义记录和位置记录。名义记录看起来与类相同,只不过使用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();
}