找回密码
 立即注册
首页 业界区 业界 记录,结构,枚举,ref,in和out 元组

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

厥轧匠 7 天前
记录

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

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

可以创建两种类型的记录:名义记录和位置记录。名义记录看起来与类相同,只不过使用record关键字代替了class关键字,如类型Book1所示。在这里,使用了只能初始化的设置访问器,禁止在创建实例后改变其状态
  1. public record Book1
  2. {
  3.         public string Title {get;set;} = string.Empty;
  4.         public string Publisher {get;set;} = string.Empty;
  5. }
复制代码
可以在记录中添加本章介绍的构造函数和其他所有成员。编译器会创建一个使用记录语法的类。记录与类的区别在于,编译器会在记录中创建另外一些功能。编译器会重写基类object的GetHashCode()和ToString()方法,创建方法和运算符重载来比较不同的值的相等性,创建方法来克隆现有对象以及创建新对象,此时可以使用对象初始化器修改一些属性的值
位置记录

实现记录的第二种方式是使用位置记录语法。这种语法在记录名称的后面使用圆括号指定成员。这种语法称为“主构造函数”。
  1. public record Book2(string Title, string Publisher);
复制代码
使用花括号可以在record中添加需要的东西,重载的构造函数,方法,或前面章节介绍的成员
  1. public record Book2(string Title, string Publisher)
  2. {
  3.         // add your members, overloads
  4. }
复制代码
对于位置记录,编译器会创建与名义记录相同的成员,并且会添加解构方法(元组中的对自定义类型的解构)。
记录的相等比较

类对于相等性比较的默认实现是比较引用。创建相同类型的两个新对象后,即使把它们实现为相同的值,它们也是不同的,因为它们引用了堆上的不同对象。
记录具有不同的行为,记录对于相等性比较的实现是,如果两个记录的属性值相同,那么它们就相等。
  1. // See https://aka.ms/new-console-template for more information
  2. A a = new("张三", 15);
  3. A a2 = new("张三", 15);
  4. Console.WriteLine(a == a2);// True
  5. Console.WriteLine(object.ReferenceEquals(a,a2));// False
  6. A a3 = new("张三", 15);
  7. B b = new("张三", 15);
  8. Console.WriteLine(a3 == b);//错误(活动)  CS0019 运算符“==”无法应用于“A”和“B”类型的操作数
  9. Console.WriteLine(object.ReferenceEquals(a3, b));// False
  10. record A(string name, int age);
  11. record B(string name, int age);
复制代码
结构

前面看到,类和记录为在程序中封装对象提供了一种出色的方式。它们被存储到堆上,让数据的生存期变得更加灵活,但性能上稍微有些损失。存储在堆上的对象需要垃圾收集器做一些工作,以便释放不再需要的对象占用的内存。为了减少垃圾收集器需要做的工作,可以为较小的对象使用栈
  1. public readonly struct Dimensions
  2. {
  3.     public Dimensions(double length, double width)
  4.     {
  5.         Length = length;
  6.                 Width = width;
  7.     }
  8.    
  9.     public double Length{get;}
  10.     public double Width{get;}
  11. }
复制代码
定义结构的成员与定义类和记录的成员的方式相同。前面已经看到了Dimensions结构的构造函数。下面的代码演示了为Dimensions结构体添加一个Diagonal属性
,它调用了Math类的Sqrt()方法
  1. public readonly struct Dimensions
  2. {
  3.     public double Diagonal => Math.Sqrt(Length * length + Width * width);
  4. }
复制代码

  • 结构采用前面讨论过的按值传递语义,即值会被复制。结构与类和记录还有其他区别:
  • 结构不支持继承。可以使用结构实现接口,但不能继承另外一个结构
  • 结构总是有一个默认的构造函数。对于类,如果定义了构造函数,则不会再生成默认构造函数。结构类型与类不同。结构总是有一个默认的构造函数,你无法创建一个自定义的无参构造函数。
  • 对于结构,可以指定字段在内存中如何布局。(见13章)
  • 结构存储在栈上,或者如果结构是堆上存储的另外一个对象的一部分,就会内联存储它们。当把结构用作对象时,如把它们传递给一个对象参数,或者调用了一个基于对象的方法,就会发生装箱,值也会被存储到堆上
枚举类型

枚举是一个值类型,包含一组命名的常量
  1. public enum Color
  2. {
  3.     Red = 0,
  4.     Blue = 1,
  5.     Green = 2
  6. }
复制代码
可以声明枚举的变量,如c1
  1. void ColorSamples()
  2. {
  3.         Color c1 = Color.Red;
  4.         Console.WriteLine(c1);
  5. }
  6. //运行结果
  7. /*
  8. Red
  9. */
复制代码
默认情况下,enum的类型是int。这个基本类型可以改为其他整数类型(byte,short,int,带符号的long和无符号的long)。命名常量的值从0开始递增,但它们可以改为其他值
  1. public enum Color : short
  2. {
  3.     Red = 1,
  4.     Blue = 2,
  5.     Green = 3
  6. }
复制代码
以下是使用枚举的顶级语句代码
  1. // 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
  2. {
  3.     Red = 0,
  4.     Blue = 1,
  5.     Green = 2
  6. }
复制代码
ref、in和out

值类型是按值传递的,所以当把一个变量赋值给另外一个变量时,列如将变量传递给方法时,将复制该变量的值。
有一种方法可以避免这种复制。如果使用ref关键字,将按引用类型传递值类型
ref
  1. int a = 1;
  2. ChangeAValueType(ref a);
  3. Console.WriteLine($"the value of changed to {a}");//输出后 a = 2;
  4. void ChangeAValueType(ref int x)
  5. {
  6.     x = 2;
  7. }
复制代码
对于不可变的string类型也可以使用ref
  1. string str1 = "hello";
  2. Console.WriteLine(str1);
  3. UpdateStringTest(ref str1);
  4. Console.WriteLine(str1);//hello2
  5. void UpdateStringTest(ref string str)
  6. {
  7.     str = "hello2";
  8. }
复制代码
如以下java代码所示,若传递对象引用给另一个方法,并在该引用上创建新对象,这样的操作将不会影响到原有的声明
在c#中 不使用ref关键字,那么c#和java的这种行为是一致的,不同的是,c#中可以通过使用ref关键字修饰对象参数
也就是说c#中对使用了ref引用的对象参数引用创建新对象,原有的对象引用会指向另一个方法中新创建的对象
  1. public class Main {
  2.     public static void main(String[] args) {
  3.         Test test = new Test();
  4.         test.setName(1);
  5.         createNewTest(test);
  6.         System.out.println("Main = " + test.getName());
  7.     }
  8.     static void createNewTest(Test test){
  9.         test = new Test(2);
  10.         System.out.println("createNewTest = " + test.getName());
  11.     }
  12. }
  13. class Test{
  14.     public int name;
  15.     public Test() {
  16.     }
  17.     public Test(int name) {
  18.         this.name = name;
  19.     }
  20.     public int getName() {
  21.         return name;
  22.     }
  23.     public void setName(int name) {
  24.         this.name = name;
  25.     }
  26. }
  27. //运行结果
  28. /*
  29. Connected to the target VM, address: '127.0.0.1:54981', transport: 'socket'
  30. createNewTest = 2
  31. Main = 1
  32. Disconnected from the target VM, address: '127.0.0.1:54981', transport: 'socket'
  33. */
复制代码
c#对自定义类使用ref示例
  1. SomeData someData = new() { Value = 1};
  2. Console.WriteLine($"调用Update前{someData}");
  3. UpdateSomeData(ref someData);
  4. Console.WriteLine($"调用Update后{someData}");
  5. void UpdateSomeData(ref SomeData someData)
  6. {
  7.     someData = new SomeData(2);
  8. }
  9. class SomeData
  10. {
  11.     public int Value { get; set; }
  12.     public SomeData()
  13.     {
  14.     }
  15.     public SomeData(int value)
  16.     {
  17.         Value = value;
  18.     }
  19.     public override string? ToString()
  20.     {
  21.         return $"Value : {Value}";
  22.     }
  23. }
  24. /*运行结果
  25. 调用Update前Value : 1
  26. 调用Update后Value : 2
  27. */
复制代码
in

如果在向方法传递一个值类型时,想要避免复制值的开销,但又不想在方法内改变值,就可以使用in修饰符
  1. void PassValueByReferenceReadonly(in SomeValue data)
  2. {
  3.     //data.Value1 = 4;//错误(活动)  CS8332 无法分配给 变量“data”的成员,或将其用作 ref 分配的右侧,因为它是只读变量
  4. }
  5. struct SomeValue
  6. {
  7.     public SomeValue(int value1, int value2, int value3, int value4)
  8.     {
  9.         Value1 = value1;
  10.         Value2 = value2;
  11.         Value3 = value3;
  12.         Value4 = value4;
  13.     }
  14.     public int Value1 { get; set; }   
  15.     public int Value2 { get; set; }   
  16.     public int Value3 { get; set; }   
  17.     public int Value4 { get; set; }   
  18. }
复制代码
ref return

为了避免方法在返回时复制值,可以在声明返回类型时添加ref关键字,并在返回值时使用return ref
  1. ref SomeValue Max(ref SomeValue x, ref SomeValue y)
  2. {
  3.     int sumx = x.Value1 + x.Value2 + x.Value3 + x.Value4;
  4.     int sumy = y.Value1 + y.Value2 + y.Value3 + y.Value4;
  5.     if (sumx > sumy)
  6.     {
  7.         return ref x;
  8.     }
  9.     else
  10.     {
  11.         return ref y;
  12.     }
  13. }
复制代码
可以使用一个条件表达式来替换if/else语句,此时需要在表达式中使用ref关键字来比较sumx和sumy,根据比较的结果,将把ref x或者 ref y
写入一个局部值的ref,然后返回该局部值的ref
  1. ref SomeValue Max(ref SomeValue x, ref SomeValue y)
  2. {
  3.     int sumx = x.Value1 + x.Value2 + x.Value3 + x.Value4;
  4.     int sumy = y.Value1 + y.Value2 + y.Value3 + y.Value4;
  5.     ref SomeValue result = ref (sumx > sumy) ? ref sumx : ref sumy;
  6.         return ref result;
  7. }
复制代码
调用者需要决定是应该复制返回的值,还是应该使用引用
  1. SomeValue one = new SomeValue(1,2,3,4);
  2. SomeValue two = new SomeValue(1,2,3,4);
  3. //将结果复制到了bigger1变量中,尽管该方法被声明为返回ref
  4. SomeValue bigger1 = Max(ref one, ref two);
  5. //使用ref 关键字来调用方法,得到一个ref return
  6. ref SomeValue bigger2 = Max(ref one, ref two);
  7. //这里使用readonly,只是为了指定bigger3变量不会被改变,如果设置属性来修改它的值,编译器将会报错
  8. ref readonly SomeValue bigger3 = Max(ref one, ref two);
复制代码
Max()方法不会修改它的任何输入。这就允许为参数使用in关键字,如MaxReadonly()方法所示,但是这里必须把返回类型的声明改为ref readonly。如果不这么做,将允许MaxReadonly()方法的调用者在收到结果后改变该方法的输入
  1. ref readonly SomeValue MaxReadonly(in SomeValue x, in SomeValue y)
  2. {
  3.     int sumx = x.Value1 + x.Value2 + x.Value3 + x.Value4;
  4.     int sumy = y.Value1 + y.Value2 + y.Value3 + y.Value4;
  5.     ref SomeValue result = ref (sumx > sumy) ? ref sumx : ref sumy;
  6.         return ref result;
  7. }
复制代码
现在调用者必须把结果写入一个ref readonly变量,或者将结果赋值到一个新的局部变量中。对于bigger5,不需要使用readonly,因为收到的原始值将被复制
  1. ref readonly SomeValue bigger4 = ref MaxReadonly(in one, in two);
  2. SomeValue bigger5 = ref MaxReadonly(in one, in two);
复制代码
out参数

如果方法应该返回多个值,那么有不同的选项可以用采用。一种选项是创建一个自定义类型,另一种选项是为参数使用ref关键字。使用ref关键字时,需要在调用方法前先初始化参数。数据将被传入方法,并从方法返回。如果方法只应该返回数据,可以使用out关键字。
  1. /*
  2.         int.Parse()方法期望收到一个string,如果解析成功,它会返回一个int。如果不能将string解析为int,将抛出一个异常。为了避免这种异常,可以使用int.TryParse()方法。无论解析是否成功,这个方法都返回一个布尔值。解析操作的结果通过一个out参数返回。
  3. */
  4. // bool TryParse(string? s, out Int32 result);
  5. /*
  6.         要调用TryParse()方法,可以使用out修饰符传递一个int。使用out修饰符时,不需要在调用TyrParse()方法前声明或者初始化该变量
  7. */
  8. Console.Write("Please enter a number: ");
  9. string? input = Console.ReadLine();
  10. if (int.TryParse(input, out int x))
  11. {
  12.     Console.WriteLine();
  13.     Console.WriteLine($"read an int:{x}");
  14. }
复制代码
元组

元组允许把多个对象组合为一个对象,但又没有创建自定义类型的复杂性
c#7开始,c#语法中集成了元组
声明和初始化元组
  1. // See https://aka.ms/new-console-template for more information
  2. Console.WriteLine("Hello, World!");
  3. void IntroTuples()
  4. {
  5.     (string AString, int Number, Book book) tuple1 = ("magic", 42, new("Professional c#", "Wrox Press"));
  6.     Console.WriteLine($"a string:{tuple1.AString},number:{tuple1.Number},book:{tuple1.book}");
  7.     /*
  8.     在把元组字面值赋值给元组变量的时候,也可以不声明其成员,此时,可以使用ValueTuple结构,
  9.         的成员名称Item1,Item2和Item3来访问元组的成员
  10.     */
  11.     var tuple2 = ("magic", 42, new Book("Professional c#", "Wrox Press"), "", "", "", "", "", "", "", "11");
  12.     Console.WriteLine($"a string:{tuple2.Item1},number:" +
  13.         $"{tuple2.Item2},book:{tuple2.Item11}");
  14.     /*
  15.     在字面值中,可以为元组字段分配名称,这需要首先定义一个名称,其后跟上一个冒号,也就是
  16.     与对象字面值相同的写法
  17.     */
  18.     var tuple3 = (AString: "magic", Number: 42, Book: new Book("Professional c#", "Wrox Press"));
  19.     Console.WriteLine($"a string:{tuple3.AString},number:" +
  20.         $"{tuple3.Number},book:{tuple3.Book}");
  21.     //类型匹配的时候,可以把一个元组赋值给另一个元组
  22.     (string a, int b, Book c) tuple4 = tuple3;
  23.     Console.WriteLine($"a string:{tuple4.a},number:{tuple4.b},book:{tuple4.c}");
  24.     /*
  25.     元组的名称也可以从源推断出来,对于变量tuple5,第二个成员是一个字符串,其值为一本书的名称
  26.     代码中没有为这个成员分配名称,但因该属性的名称为Title,所以将自动使用Title作为元组的名称
  27.      */
  28.     Book book = new Book("Professional c#", "Wrox Press");
  29.     var tuple5 = (Number:42,book.Title);
  30.     Console.WriteLine($"Number:{tuple5.Number},book:{tuple5.Title}");
  31. }
  32. IntroTuples();
  33. class Book
  34. {
  35.     public String Title { get; set; }
  36.     public String Publisher { get; set; }
  37.     public Book(String Title, String Publisher)
  38.     {
  39.         this.Title = Title;
  40.         this.Publisher = Publisher;
  41.     }
  42. }
复制代码
元组解构
  1. void TupleDeconstruction()
  2. {
  3.     var tuple1 = (AString: "magic", Number: 42, Book: new Book("Professional c#", "Wrox Press"));
  4.     (string AString, int Number, Book book) = tuple1;
  5.     Console.WriteLine($"a string:{AString},number:{Number},book:{book}");
  6.     //如果不需要某些变量,可以使用discard,discard是名称为_的c#占位符变量。它们用来忽略结果
  7.     (_, _, Book book1) = tuple1;
  8.     Console.WriteLine($"book:{book1.Title}");
  9. }
  10. TupleDeconstruction();
复制代码
元组的返回
  1. static (int result, int remainder) Divide(int dividend, int divisor)
  2. {
  3.     int result = dividend / divisor;
  4.     int remainder = dividend % divisor;
  5.     return (result, remainder);
  6. }
  7. static void ReturningTuples()
  8. {
  9.     (int result, int remainder) = Divide(7, 2);
  10.     Console.WriteLine($"7 / 2 - result: {result}, remainder: {remainder}");
  11. }
  12. ReturningTuples();
复制代码
元组的值传递

元组的引用传递是值传递,原因在于,在为元组使用c#语法时,编译器在后台会使用ValueTuple类型(这是一个结构)并复制值
  1. // See https://aka.ms/new-console-template for more information
  2. Console.WriteLine("Hello, World!");
  3. (string a, int b, short c)= ("A", 1, 2);
  4. (string a1, int b2, short cd) = (a, b, c);
  5. a1 = "B";
  6. Console.WriteLine("a="+a);
  7. Console.WriteLine("a1=" + a1);
  8. /*
  9. * 输出
  10. Hello, World!
  11. a=A
  12. a1=B
  13. */
复制代码
  1. // See https://aka.ms/new-console-template for more information
  2. Console.WriteLine("Hello, World!");
  3. (string a, int b, short c) a= ("A", 1, 2);
  4. (string a, int b, short c) b = a;
  5. b.a = "B";
  6. b.b = 5;
  7. b.c = 6;
  8. Console.WriteLine("元组b的a="+b.a);
  9. Console.WriteLine("元组a的a=" + a.a);
  10. /*
  11. * 输出
  12. 元组b的a=B
  13. 元组a的a=A
  14. */
复制代码
对自定义类型的解构

为完成自定义类型的解构,只需要创建Deconstruct()方法(也被称为解构器),将分离的部分放入out参数中
  1. Person person = new("first", "last", 42);
  2. (string firstName, string lastName, int age) = person;
  3. Console.WriteLine($"firstName:{firstName},lastName:{lastName},age:{age}");
  4. class Person
  5. {
  6.     public string FirstName { get; set; }
  7.     public string LastName { get; set; }
  8.     public int Age { get; set; }
  9.     public Person()
  10.     {
  11.     }
  12.     public Person(string firstName, string lastName, int age)
  13.     {
  14.         FirstName = firstName;
  15.         LastName = lastName;
  16.         Age = age;
  17.     }
  18.     public void Deconstruct(out string firstName, out string lastName, out int age)
  19.     {
  20.         firstName = FirstName;
  21.         lastName = LastName;
  22.         age = Age;
  23.     }
  24. }
复制代码
模式匹配

使用is null 和is not null判断是否为空
  1. int? i = null; //bool b = i.HasValue;
  2. Console.WriteLine(i is null); //True
  3. Console.WriteLine(i is not null); //False
复制代码
分部类型

partial关键字可用于class struct interface前
  1. // See https://aka.ms/new-console-template for more information
  2. Console.WriteLine("Hello, World!");
  3. SampleClass s = new ();
  4. s.MethodOne();
  5. s.MethodTwo();
  6. //当编译包含这两个源文件的同时,会创建一个SampleClass类,它有两个方法MethodTwo(),MethodOne()
  7. //所有的特性,XML注释,接口,泛型参数特性和成员会合并
  8. //SampleClassAutoGenerated.cs
  9. using System;
  10. partial class SampleClass
  11. {
  12.     public void MethodTwo()
  13.     {
  14.         Console.WriteLine("MethodTwo方法");
  15.     }
  16.    
  17.     //如果不返回void就必须在另一个分部类里提供实现
  18.     public partial void APartialMethod()
  19.     {
  20.         Console.WriteLine("APartialMethod方法");
  21.     }
  22. }
  23. //SampleClass.cs
  24. using System;
  25. partial class SampleClass
  26. {
  27.     public void MethodOne()
  28.     {
  29.         Console.WriteLine("MethodOne方法");
  30.                 //如果另一个分部类没有提供实现,则编译器会忽略该调用
  31.                 APartialMethod();
  32.     }
  33.         public partial void APartialMethod();
  34. }
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册