厥轧匠 发表于 2025-10-6 11:39:54

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

记录

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

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

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

前面看到,类和记录为在程序中封装对象提供了一种出色的方式。它们被存储到堆上,让数据的生存期变得更加灵活,但性能上稍微有些损失。存储在堆上的对象需要垃圾收集器做一些工作,以便释放不再需要的对象占用的内存。为了减少垃圾收集器需要做的工作,可以为较小的对象使用栈
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 informationConsole.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, 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);//hello2

void 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();
}
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 记录,结构,枚举,ref,in和out 元组