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

1

9

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.

Shiner answered 17/10, 2021 at 18:49 Comment(3)
It's still a struct. Records aren't a new kind of type, they're new behavior over the existing types. In fact, without the readonly keyword a record struct is mutable, just like any other structTalc
PS: Why not use a Vector3 which would allow you to use SIMD operations?Talc
@PanagiotisKanavos Is there a Vector3 for doubles? There are some nice convenient features like having access to add properties as needed, or constructor overloading like doing Vec3 v = (1, 2, 3);. There is one worry I have which is here saying "In general the performance benefit of using SIMD varies depending on the specific scenario, and in some cases it can even perform worse than simpler non-SIMD equivalent code." Not saying these are great reasons, and I'm more than open to be convinced of changing.Shiner
T
16

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.

Talc answered 19/10, 2021 at 6:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.