【C#】【值类型】

115 阅读6分钟

概述

值类型的变量包含类型的实例。对值类型变量的操作只影响存储在变量中的值类型实例。

如果值类型包含引用类型的数据成员,则在复制值类型实例时,只会复制对引用类型实例的引用。 副本和原始值类型实例都具有对同一引用类型实例的访问权限。

public struct MutablePoint
 {
    public int X;
    public int Y;
    public MutablePoint(int x, int y) =>(X,Y) = (x,y);

    public override string ToString() =>  $"({X},{Y})";
 }

 public class Program
 {
    public static void Main()
    {
        var p1 = new MutablePoint(1,2);
        var p2 = p1;
        p2.Y = 200;
        Console.WriteLine($"{nameof(p1)} after {nameof(p2)} is modified:{p1}");
        Console.WriteLine($"{nameof(p2)}:{p2}");

        
        MutateAndDisplay(p2);
        Console.WriteLine($"{nameof(p2)} after passing to a method: {p2}");
    }

    private static void MutateAndDisplay(MutablePoint p)
    {
        p.X = 100;
        Console.WriteLine($"Point mutated in a method: {p}");
    }
 }
 
 
 
 //输出结果
p1 after p2 is modified:(1,2)
p2:(1,200)
Point mutated in a method: (100,200)
p2 after passing to a method: (1,200)

枚举类型

默认情况下,枚举成员的关联常数值为类型 int;它们从零开始,并按定义文本顺序递增 1。 可以显式指定任何其他整数数值类型作为枚举类型的基础类型。 还可以显式指定关联的常数值。

使用 [Enum.IsDefined]方法来确定枚举类型是否包含具有特定关联值的枚举成员。

 public enum Season
 {
    Spring = 0,
    Summer,
    Fall,
    Winter
 }

 public class Program
 {
    public static void Main()
    {
        Season a = Season.Spring;
        Console.WriteLine($"Integral value of {a} is {(int)a}");

        var b = Season.Summer;
        Console.WriteLine(b);
        object value;
        value = 1;
        Console.WriteLine("{0}:{1}",value,Enum.IsDefined(typeof(Season),value));
    }
 }

作为标志位的枚举类型

如果希望枚举类型表示组合选项,为每一个枚举成员定位位标志,通过与或非逻辑运算符关联组合。 要记得加上[Flags]标志。

[Flags]
 public enum Days
 {
   None         = 0b_0000_0000,
   Monday       = 0b_0000_0001,
   Tuesday      = 0b_0000_0010,
   Wednesday    = 0b_0000_0100,
   Thursday     = 0b_0000_1000,
   Friday       = 0b_0001_0000,
   Saturday     = 0b_0010_0000,
   Sunday       = 0b_0100_0000,
   Weekend = Saturday | Sunday
 }

 public class Program
 {
    public static void Main()
    {
        Days meetingDays = Days.Monday | Days.Tuesday;
        Console.WriteLine(meetingDays);

        bool isMeetingOnTuesday = (meetingDays & Days.Tuesday) == Days.Tuesday;
        Console.WriteLine($"Is there a meeting on Tuesday:{isMeetingOnTuesday}");
    }

 }

结构类型

是一种可封装数据和相关功能的值类型。通常,可以使用结构类型来设计以数据为中心的较小类型,这些类型只有很少的行为或没有行为。由于结构类型具有值语义,因此建议定义不可变的结构类型。

public struct Coords
 {
    public Coords(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double X { get;}
    public double Y { get;}

    public override string ToString() => $"({X},{Y})";
 }

readonly结构

可以使用 readonly 修饰符来声明结构类型为不可变。 readonly 结构的所有数据成员都必须是只读的,任何属性都必须是只读的或者仅 init。 这样可以保证readonly结构的成员不狐疑修改该结构的状态,除构造函数外的其他实例成员是隐式readonly.

 public readonly struct Coords
 {
    public Coords(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double X { get;init;}
    public double Y { get;init;}

    public override string ToString() => $"({X},{Y})";
 }

 public class Program
 {
    public static void Main()
    {
       Coords point1 = new Coords(1, 1);
       Console.WriteLine(point1);
    }

 }

如果不能将整个结构类型声明为 readonly,可使用 readonly 修饰符标记不会修改结构状态的实例成员。

在 readonly 实例成员内,不能分配到结构的实例字段。 但是,readonly 成员可以调用非 readonly 成员。 在这种情况下,编译器将创建结构实例的副本,并调用该副本上的非 readonly 成员。 因此,不会修改原始结构实例。 可以将 readonly 修饰符应用于结构类型的静态字段,但不能应用于任何其他静态成员,例如属性或方法。

 public  struct Coords
 {
    public Coords(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double X { get;init;}
    public double Y { get;init;}

    public readonly override string ToString() => $"({X},{Y})";

    public readonly double sum()
    {
        return X + Y;
    }
 }

 public class Program
 {
    public static void Main()
    {
       Coords point1 = new Coords(1, 1);
       Console.WriteLine(point1);
       Console.WriteLine(point1.sum());
    }

 }

修改值的副本

从 C# 10 开始,可以使用 with 表达式来生成修改了指定属性和字段的结构类型实例的副本。

  public static void Main()
    {
       Coords point1 = new Coords(1, 1);
       Console.WriteLine(point1);
       Coords point2 = point1 with{X = 3};
       Console.WriteLine(point2);
       Console.WriteLine(point1);
    }
    
    //输出为
    (1,1)
    (3,1)
    (1,1)

内连数组

从 C# 12 开始,可以将内联数组声明为 struct 类型:

[System.Runtime.CompilerServices.InlineArray(10)]
 public struct Buffer
 {
    private int _firstElement;
 }

 public class Program
 {
    public static void Main()
    {
        var buffer = new Buffer();
        for(int i = 0; i < 10;i++)
        {
            buffer[i] = i;
        }

        foreach(int c in buffer)
        {
            Console.WriteLine(c);
        }
    }

 }

结构初始化和默认值

struct 类型的变量直接包含该 struct 类型的数据。 这会让未初始化的 struct(具有其默认值)和已初始化的 struct(通过构造值来存储一组值)之间存在区别。

使用default忽略了无参数构造函数,并生成了结构类型的默认值。

结构类型数组实例化还忽略无参数构造函数并生成使用结构类型的默认值填充的数组。

如果结构类型的所有实例字段都是可访问的,则还可以在不使用 new 运算符的情况下对其进行实例化。 在这种情况下,在首次使用实例之前必须初始化所有实例字段。

public readonly struct Measurement
{
    public Measurement()
    {
        Value = double.NaN;
        Description = "Undefined";
    }

    public Measurement(double value, string description)
    {
        Value = value;
        Description = description;
    }

    public double Value { get; init; }
    public string Description { get; init; }

    public override string ToString() => $"{Value} ({Description})";
}

public static void Main()
{
    var m1 = new Measurement();
    Console.WriteLine(m1);  // output: NaN (Undefined)

    var m2 = default(Measurement);
    Console.WriteLine(m2);  // output: 0 ()

    var ms = new Measurement[2];
    Console.WriteLine(string.Join(", ", ms));  // output: 0 (), 0 ()
    
}


//必须初始化所有实例字段
public static class StructWithoutNew
{
    public struct Coords
    {
        public double x;
        public double y;
    }

    public static void Main()
    {
        Coords p;
        p.x = 3;
        p.y = 4;
        Console.WriteLine($"({p.x}, {p.y})");  // output: (3, 4)
    }
}

将结构类型变量作为参数传递给方法或从方法返回结构类型值时,将复制结构类型的整个实例。 通过值传递可能会影响高性能方案中涉及大型结构类型的代码的性能。 通过按引用传递结构类型变量,可以避免值复制操作。 使用 refoutin 或 ref readonly 方法参数修饰符,指示必须按引用传递某个参数。

元组类型

类似 MATLAB 元胞数组,可以将多个数据元素分组成一个轻型数据结构。

元组类型的用例

(double,int) t1 = (4.5,3);
 Console.WriteLine($"Tuple with elements {t1.Item1} and {t1.Item2}");
 Console.WriteLine($"Hash code of {t1} is {t1.GetHashCode()}.");

//out
//Tuple with elements 4.5 and 3
//Hash code of (4.5, 3) is 1455877892.


 (double Sum, int count) t2 = (4.5,3);
 Console.WriteLine($"sum of {t2.count} element is {t2.Sum}");

 (double Sum, int Count) t3 = (4.5,3);
 Console.WriteLine($"sum of {t3.Count} element is {t3.Sum}");

 var t4 = (Sum: 4.5, Count: 3);
Console.WriteLine($"sum of {t4.Count} element is {t4.Sum}");

var sum = 4.5;
var count = 3;
var t5 = (sum,count);
Console.WriteLine($"Sum of {t5.count} elements is {t5.sum}");

元组字段名称

可以在元组初始化表达式中或元组类型定义中显式指定元组字段名称:

(double Sum, int Count) t3 = (4.5,3);
 Console.WriteLine($"sum of {t3.Count} element is {t3.Sum}");

如果未指定字段名称,可以根据元组初始化表达式中相应变量的名称推断出此名称:

var sum = 4.5;
var count = 3;
var t5 = (sum,count);
Console.WriteLine($"Sum of {t5.count} elements is {t5.sum}");

在以下情况下,变量名称不会被投影到元组字段名称中:

  • 候选名称是元素类型的成员名称,例如 Item3,ToStringRest.
  • 候选名称是另一元组的显式或隐式字段名称的重复项。

元组字段的默认名称为Item1,Item2,Item3。始终可以使用字段的默认名称,即使字段名称是显式指定的或推断出来的。

var a = 1;
var t = (a , b: 2, 3);
Console.WriteLine($"The 1st element is {t.Item1}(same as {t.a}).");
Console.WriteLine($"The 2nd element is {t.Item2}(same as {t.b}).");
Console.WriteLine($"The 3rd element is {t.Item3}");

//注意:
t(1,2,3) t(a,b,Item3)

从 C# 12 开始,可以使用 using 指令指定元组类型的别名。

global using BandPass = (int Min, int Max);
BandPass bracket = (40,100);

Console.WriteLine($"The bandpass filter is {bracket.Min} to {bracket.Max}.");

别名不会引入新类型,而只会为现有类型创建同义词。

元组赋值和析构

满足以下两个条件的元组类型可以赋值:

  • 两个元组类型有相同数量的元素
  • 对于每个元组位置,右侧元组元素的类型与左侧相应的元组元素的类型相同或可以隐式转换为左侧相应的元组元素的类型

元组元素是按照元素的顺序赋值的。元组字段的名称会被忽略且不会被赋值:

(int,double) t1 = (17,3.14);
(double First,double Second) t2 = (0.0,1.0);

t2 = t1;
Console.WriteLine($"{nameof(t2)}:{t2.First} and {t2.Second}");

(double A, double B) t3 = (2.0,3.0);
t3 = t2;
Console.WriteLine($"{nameof(t3)}:{t3.A} and {t3.B}");

元组相等

元组类型支持 == 和 != 运算符。 这些运算符按照元组元素的顺序将左侧操作数的成员与相应的右侧操作数的成员进行比较。== 和 != 操作不会考虑元组字段名称。

元组的用例

元组最常见的用例之一是作为方法返回类型。 也就是说,你可以将方法结果分组为元组返回类型,而不是定义 out 方法参数,

int[] xs = new int[] { 4, 7, 9 }; var limits = FindMinMax(xs);

(int min, int max) FindMinMax(int[] input)
{
    if (input is null || input.Length == 0)
    {
        throw new ArgumentException("Cannot find minimum and maximum of a null or empty array.");
    }

    var min = int.MaxValue;
  
    var max = int.MinValue;
    foreach (var i in input)
    {
        if (i < min)
        {
            min = i;
        }
        if (i > max)
        {
            max = i;
        }
    }
    return (min, max);
}

可为空的值的类型(C# 参考)

T? 表示其基础值类型T 的所有值及额外的null 值。

bool? flag = null;

Console.WriteLine(flag);

检查可为空值类型的实例

可以将 is 运算符与类型模式 结合使用,既检查 null 的可为空值类型的实例,又检索基础类型的值:

int? a = 42;
if(a is int valueOfA)
{
    Console.WriteLine($"a is {valueOfA}");
}
else
{
    Console.WriteLine("a does not have a value");
}

从可为空的值类型转换为基础类型

?? null 合并符操作

int? a = 28;
int b = a ?? -1;
Console.WriteLine($"b is {b}");  //28

int? c = null;
int d = c ?? -1;
Console.WriteLine($"d is {d}"); //-1

对于比较运算符<><= 和 >=,如果一个或全部两个操作数都为 null,则结果为 false; 对于相等运算符==,如果两个操作数都为 null,则结果为 true;如果只有一个操作数为 null,则结果为 false; 对于不等运算符!=,如果两个操作数都为 null,则结果为 false;如果只有一个操作数为 null,则结果为 true