Search code examples
c#structlayoutc#-10.0

Do C#10’s readonly record structs guarantee the same size and alignment of fields as the explicit implementation?


I do stuff where having contiguous data is required. Now with C# 10, we can do public readonly record struct.

I like having the automatic ToString feature that records have, among others, so having that done for me is nice.

As such, are the following equivalent?

[StructLayout(LayoutKind.Sequential, Pack = 4)]
public readonly struct MyVector
{
    public readonly float X;
    public readonly float Y;
    public readonly float Z;

    public MyVector(float x, float y, float z)
    {
        X = x;
        Y = y;
        Z = z;
    }
}

versus the nicely condensed C# 10 version

[StructLayout(LayoutKind.Sequential, Pack = 4)]
public readonly record struct MyVectorRecord(float X, float Y, float Z)
{
}

Or are there any landmines I'm going to accidentally step on doing this? By which I mean are there any things being done under the hood by record that make what I've written above not do what I want with respect to contiguous packing? I can't have the record insert padding, spacing, or do anything weird.

I am not using a vector class with record structs and was using this for purposes of illustration. You can ignore things like "floating point equality comparisons" since I am only interested in whether I can pass this off to a library that is expecting a contiguous sequence of X/Y/Z's.


Solution

  • record isn't a new type, it's specific behavior applied to reference and now value types. The struct remains a struct. You can test this at sharplab.io, to see the code generated by the compiler in each case.

    A record uses properties though, not raw fields, so you can only compare structs with properties to record structs. That's the important difference

    This struct:

    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    public readonly struct MyVectorRecord2
    { 
        public float X {get;} 
        public float Y {get;} 
        public float Z {get;}
        
         public MyVectorRecord2(float x, float y, float z)
        {
            X = x;
            Y = y;
            Z = z;
        }
    }
    

    produces

    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    [IsReadOnly]
    public struct MyVectorRecord2
    {
        [CompilerGenerated]
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private readonly float <X>k__BackingField;
    
        [CompilerGenerated]
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private readonly float <Y>k__BackingField;
    
        [CompilerGenerated]
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private readonly float <Z>k__BackingField;
    
        public float X
        {
            [CompilerGenerated]
            get
            {
                return <X>k__BackingField;
            }
        }
    
        public float Y
        {
            [CompilerGenerated]
            get
            {
                return <Y>k__BackingField;
            }
        }
    
        public float Z
        {
            [CompilerGenerated]
            get
            {
                return <Z>k__BackingField;
            }
        }
    
        public MyVectorRecord2(float x, float y, float z)
        {
            <X>k__BackingField = x;
            <Y>k__BackingField = y;
            <Z>k__BackingField = z;
        }
    }
    

    While the record

    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    public readonly record struct MyVectorRecord(float X, float Y, float Z)
    {
    }
    

    produces:

    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    [IsReadOnly]
    public struct MyVectorRecord : IEquatable<MyVectorRecord>
    {
        [CompilerGenerated]
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private readonly float <X>k__BackingField;
    
        [CompilerGenerated]
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private readonly float <Y>k__BackingField;
    
        [CompilerGenerated]
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private readonly float <Z>k__BackingField;
    
        public float X
        {
            [CompilerGenerated]
            get
            {
                return <X>k__BackingField;
            }
            [CompilerGenerated]
            init
            {
                <X>k__BackingField = value;
            }
        }
    
        public float Y
        {
            [CompilerGenerated]
            get
            {
                return <Y>k__BackingField;
            }
            [CompilerGenerated]
            init
            {
                <Y>k__BackingField = value;
            }
        }
    
        public float Z
        {
            [CompilerGenerated]
            get
            {
                return <Z>k__BackingField;
            }
            [CompilerGenerated]
            init
            {
                <Z>k__BackingField = value;
            }
        }
    
        public MyVectorRecord(float X, float Y, float Z)
        {
            <X>k__BackingField = X;
            <Y>k__BackingField = Y;
            <Z>k__BackingField = Z;
        }
    
        public override string ToString()
        {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.Append("MyVectorRecord");
            stringBuilder.Append(" { ");
            if (PrintMembers(stringBuilder))
            {
                stringBuilder.Append(' ');
            }
            stringBuilder.Append('}');
            return stringBuilder.ToString();
        }
    
        private bool PrintMembers(StringBuilder builder)
        {
            builder.Append("X = ");
            builder.Append(X.ToString());
            builder.Append(", Y = ");
            builder.Append(Y.ToString());
            builder.Append(", Z = ");
            builder.Append(Z.ToString());
            return true;
        }
    
        public static bool operator !=(MyVectorRecord left, MyVectorRecord right)
        {
            return !(left == right);
        }
    
        public static bool operator ==(MyVectorRecord left, MyVectorRecord right)
        {
            return left.Equals(right);
        }
    
        public override int GetHashCode()
        {
            return (EqualityComparer<float>.Default.GetHashCode(<X>k__BackingField) * -1521134295 + EqualityComparer<float>.Default.GetHashCode(<Y>k__BackingField)) * -1521134295 + EqualityComparer<float>.Default.GetHashCode(<Z>k__BackingField);
        }
    
        public override bool Equals(object obj)
        {
            return obj is MyVectorRecord && Equals((MyVectorRecord)obj);
        }
    
        public bool Equals(MyVectorRecord other)
        {
            return EqualityComparer<float>.Default.Equals(<X>k__BackingField, other.<X>k__BackingField) && EqualityComparer<float>.Default.Equals(<Y>k__BackingField, other.<Y>k__BackingField) && EqualityComparer<float>.Default.Equals(<Z>k__BackingField, other.<Z>k__BackingField);
        }
    
        public void Deconstruct(out float X, out float Y, out float Z)
        {
            X = this.X;
            Y = this.Y;
            Z = this.Z;
        }
    }
    

    Finally, this

    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    public readonly struct MyVector
    {
        public readonly float X;
        public readonly float Y;
        public readonly float Z;
    
        public MyVector(float x, float y, float z)
        {
            X = x;
            Y = y;
            Z = z;
        }
    }
    

    Remains unchanged, apart from the IsReadOnly attribute.

    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    [IsReadOnly]
    public struct MyVector
    {
        public readonly float X;
    
        public readonly float Y;
    
        public readonly float Z;
    
        public MyVector(float x, float y, float z)
        {
            X = x;
            Y = y;
            Z = z;
        }
    }
    

    The big difference is between structs with fields and structs with public properties. After that, a record struct contains only extra methods compared to a struct with properties.