7

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.

user2864740
  • 60,010
  • 15
  • 145
  • 220
Water
  • 3,245
  • 3
  • 28
  • 58
  • 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 `struct` – Panagiotis Kanavos Oct 19 '21 at 06:12
  • PS: Why not use a `Vector3` which would allow you to use SIMD operations? – Panagiotis Kanavos Oct 19 '21 at 06:17
  • @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](https://learn.microsoft.com/en-us/dotnet/standard/simd) 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. – Water Oct 26 '21 at 20:45

1 Answers1

10

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.

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236