Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disable anchors in serialized yaml #404

Closed
wants to merge 1 commit into from

Conversation

WEGFan
Copy link
Member

@WEGFan WEGFan commented Dec 23, 2021

If the object has multiple same values (checked by Equals() and GetHashCode()), the serializer will use anchors by default to reduce file size. However, those values will share the same reference after deserializing, causing some unexpected behaviors.

internal class Program {
    public record Point {
        public int X { get; set; }
        public int Y { get; set; }

        public Point() {
        }

        public Point(int x, int y) {
            X = x;
            Y = y;
        }
    }

    public record Node {
        public Point Position { get; set; }
        public int Scale { get; set; }
    }

    public static void Main(string[] args) {
        List<Node> nodes = new List<Node> {
            new Node {Position = new Point(0, 0), Scale = 1},
            new Node {Position = new Point(0, 0), Scale = 1},
            new Node {Position = new Point(0, 0), Scale = 2}
        };

        string yaml = YamlHelper.Serializer.Serialize(nodes);
        // Outputs:
        // - &o0
        //   Position: &o1
        //     X: 0
        //     Y: 0
        //   Scale: 1
        // - *o0
        // - Position: *o1
        //   Scale: 2
        Console.WriteLine(yaml);

        List<Node> result = YamlHelper.Deserializer.Deserialize<List<Node>>(yaml);
        // Outputs: True True
        Console.WriteLine(ReferenceEquals(result[0], result[1]));
        Console.WriteLine(ReferenceEquals(result[0].Position, result[2].Position));
    }
}

Suppose the player wants to change the scale of the first node in mod settings, and the code is result[0].Scale = 2, then the second node is also changed.

@0x0ade
Copy link
Member

0x0ade commented Dec 27, 2021

This seems like a possible YamlDotNet upstream issue to me, which should be using ReferenceEquals instead of Equals and GetHashCode for this kind of thing. Maybe worth noting in aaubry/YamlDotNet#571?

@WEGFan
Copy link
Member Author

WEGFan commented Dec 28, 2021

Actually non-record classes have this issue too:

public class Point : IEquatable<Point> {
    public int X { get; set; }
    public int Y { get; set; }

    public Point() {}
    public Point(int x, int y) {
        X = x;
        Y = y;
    }

    public bool Equals(Point other) {
        if (ReferenceEquals(null, other)) {
            return false;
        }
        if (ReferenceEquals(this, other)) {
            return true;
        }
        return X == other.X && Y == other.Y;
    }

    public override bool Equals(object obj) {
        if (ReferenceEquals(null, obj)) {
            return false;
        }
        if (ReferenceEquals(this, obj)) {
            return true;
        }
        if (obj.GetType() != this.GetType()) {
            return false;
        }
        return Equals((Point)obj);
    }

    public override int GetHashCode() {
        unchecked {
            return (X * 397) ^ Y;
        }
    }
}

public static void Main(string[] args) {
    List<Point> points = new List<Point> {
        new Point(0, 0),
        new Point(0, 0)
    };
    // - &o0
    //   X: 0
    //   Y: 0
    // - *o0
    YamlHelper.Serializer.Serialize(points);
}

It uses a dictionary to store anchors in code, and uses the object as a key, so Equals() and GetHashCode() is used.

According to the doc:

When the same object appears multiple times in the serialized graph, the default behaviour is to emit it only once, assigning it an anchor, and use aliases to reference it on its subsequent occurrences.

I assume this is intended to decrease the output size (and maybe to solve cyclic reference issue?), rather than to let the output document have a closer structure with the original object. 😅

@0x0ade
Copy link
Member

0x0ade commented Dec 28, 2021

The docs also talk about "duplicate references", which means that they really should be using an equality comparer which uses ReferenceEquals 😅

@0x0ade
Copy link
Member

0x0ade commented Jan 23, 2022

Given that there haven't been any developments upstream regarding this / the related issue, I'd like to revisit the idea mentioned on (I think it was) Discord before falling back to merging this PR. Would you or anyone else be willing to try implementing a fix that changes the value equality checks to reference equality checks? Or should I attempt it in my spare time?

@WEGFan
Copy link
Member Author

WEGFan commented Jan 23, 2022

I'll try it these days.

@WEGFan
Copy link
Member Author

WEGFan commented Feb 15, 2022

Currently stuck because of aaubry/YamlDotNet#677, if there's still no response then I guess I need to use reflections to solve that. 😅

@WEGFan WEGFan marked this pull request as draft June 28, 2022 03:51
@maddie480 maddie480 added the dormant This PR is really old, do we really need it? label Apr 29, 2024
@WEGFan WEGFan closed this Aug 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dormant This PR is really old, do we really need it?
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants