Skip to content

Commit

Permalink
Merge pull request #268 from Cysharp/hadashiA/skip-overwrite-by-default
Browse files Browse the repository at this point in the history
Add attr to not overwrite Member with default value
  • Loading branch information
hadashiA committed Mar 28, 2024
2 parents aa947b3 + 5cc81fc commit 4cefdad
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 52 deletions.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ public partial class Person3
When serializing/deserializing, MemoryPack can invoke a before/after event using the `[MemoryPackOnSerializing]`, `[MemoryPackOnSerialized]`, `[MemoryPackOnDeserializing]`, `[MemoryPackOnDeserialized]` attributes. It can annotate both static and instance (non-static) methods, and public and private methods.

```csharp
[MemoryPackable]
[MemoryPackable]
public partial class MethodCallSample
{
// method call order is static -> instance
Expand Down Expand Up @@ -546,6 +546,28 @@ public partial class VersionCheck

In use-case, store old data (to file, to redis, etc...) and read to new schema is always ok. In the RPC scenario, schema exists both on the client and the server side, the client must be updated before the server. An updated client has no problem connecting to the old server but an old client can not connect to a new server.


By default, when the old data read to new schema, any members not on the data side are initialized with the `default` literal.
If you want to avoid this and use initial values of field/properties, you can use `[SuppressDefaultInitialization]`.

```cs
[MemoryPackable]
public partial class DefaultValue
{
public string Prop1 { get; set; }

[SuppressDefaultInitialization]
public int Prop2 { get; set; } = 111; // < if old data is missing, set `111`.
public int Prop3 { get; set; } = 222; // < if old data is missing, set `default`.
}
```

`[SuppressDefaultInitialization]` has following disadvantages:
- Cannot be used with readonly, init-only, and required modifier.
=- May not be the best performance due to increased conditional branching. (But it would be negligible.)


The next [Serialization info](#serialization-info) section shows how to check for schema changes, e.g., by CI, to prevent accidents.

When using `GenerateType.VersionTolerant`, it supports full version-tolerant.
Expand Down
3 changes: 3 additions & 0 deletions src/MemoryPack.Core/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,6 @@ public sealed class MemoryPackOnDeserializedAttribute : Attribute
public sealed class GenerateTypeScriptAttribute : Attribute
{
}

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class SuppressDefaultInitialization : Attribute;
8 changes: 8 additions & 0 deletions src/MemoryPack.Generator/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -324,4 +324,12 @@ internal static class DiagnosticDescriptors
category: Category,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor SuppressDefaultInitializationMustBeSettable = new(
id: "MEMPACK040",
title: "Readonly member cannot specify [SuppressDefaultInitialization]",
messageFormat: "The MemoryPackable object '{0}' member '{1}' has [SuppressDefaultInitialization], it cannot be readonly, init-only and required.",
category: Category,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
}
18 changes: 16 additions & 2 deletions src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -539,14 +539,15 @@ private string EmitDeserializeBody()

SET:
{{(!IsUseEmptyConstructor ? "goto NEW;" : "")}}
{{Members.Where(x => x.Symbol != null).Where(x => x.IsAssignable).Select(x => $" {(IsUseEmptyConstructor ? "" : "// ")}value.@{x.Name} = __{x.Name};").NewLine()}}
{{Members.Where(x => x.IsAssignable).Select(x => $" {(IsUseEmptyConstructor ? "" : "// ")}value.@{x.Name} = __{x.Name};").NewLine()}}
goto READ_END;

NEW:
value = {{EmitConstructor()}}
{
{{EmitDeserializeConstruction(" ")}}
};
{{EmitDeserializeConstructionWithBranching(" ")}}
READ_END:
{{readEndBody}}
""";
Expand Down Expand Up @@ -916,10 +917,23 @@ string EmitDeserializeConstruction(string indent)
{
// all value is deserialized, __Name is exsits.
return string.Join("," + Environment.NewLine, Members
.Where(x => x.IsSettable && !x.IsConstructorParameter)
.Where(x => x is { IsSettable: true, IsConstructorParameter: false, SuppressDefaultInitialization: false })
.Select(x => $"{indent}@{x.Name} = __{x.Name}"));
}

string EmitDeserializeConstructionWithBranching(string indent)
{
var members = Members
.Select((x, i) => (x, i))
.Where(v => v.x.SuppressDefaultInitialization);

var lines = GenerateType is GenerateType.VersionTolerant or GenerateType.CircularReference
? members.Select(v => $"{indent}if (deltas.Length > {v.i} && deltas[{v.i}] != 0) value.@{v.x.Name} = __{v.x.Name};")
: members.Select(v => $"{indent}if ({v.i + 1} <= count) value.@{v.x.Name} = __{v.x.Name};");

return lines.NewLine();
}

string EmitUnionTemplate(IGeneratorContext context)
{
var classOrInterfaceOrRecord = IsRecord ? "record" : (Symbol.TypeKind == TypeKind.Interface) ? "interface" : "class";
Expand Down
7 changes: 7 additions & 0 deletions src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,11 @@ public bool Validate(TypeDeclarationSyntax syntax, IGeneratorContext context, bo
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.ReadOnlyFieldMustBeConstructorMember, item.GetLocation(syntax), Symbol.Name, item.Name));
noError = false;
}
else if (item is { SuppressDefaultInitialization: true, IsAssignable: false })
{
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SuppressDefaultInitializationMustBeSettable, item.GetLocation(syntax), Symbol.Name, item.Name));
noError = false;
}
}
}

Expand Down Expand Up @@ -615,6 +620,7 @@ partial class MemberMeta
public int Order { get; }
public bool HasExplicitOrder { get; }
public MemberKind Kind { get; }
public bool SuppressDefaultInitialization { get; }

MemberMeta(int order)
{
Expand All @@ -630,6 +636,7 @@ public MemberMeta(ISymbol symbol, IMethodSymbol? constructor, ReferenceSymbols r
this.Symbol = symbol;
this.Name = symbol.Name;
this.Order = sequentialOrder;
this.SuppressDefaultInitialization = symbol.ContainsAttribute(references.SkipOverwriteDefaultAttribute);
var orderAttr = symbol.GetAttribute(references.MemoryPackOrderAttribute);
if (orderAttr != null)
{
Expand Down
4 changes: 3 additions & 1 deletion src/MemoryPack.Generator/ReferenceSymbols.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class ReferenceSymbols
public INamedTypeSymbol MemoryPackOnSerializedAttribute { get; }
public INamedTypeSymbol MemoryPackOnDeserializingAttribute { get; }
public INamedTypeSymbol MemoryPackOnDeserializedAttribute { get; }
public INamedTypeSymbol SkipOverwriteDefaultAttribute { get; }
public INamedTypeSymbol GenerateTypeScriptAttribute { get; }
public INamedTypeSymbol IMemoryPackable { get; }

Expand All @@ -46,6 +47,7 @@ public ReferenceSymbols(Compilation compilation)
MemoryPackOnSerializedAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackOnSerializedAttribute");
MemoryPackOnDeserializingAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackOnDeserializingAttribute");
MemoryPackOnDeserializedAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackOnDeserializedAttribute");
SkipOverwriteDefaultAttribute = GetTypeByMetadataName("MemoryPack.SuppressDefaultInitialization");
GenerateTypeScriptAttribute = GetTypeByMetadataName(MemoryPackGenerator.GenerateTypeScriptAttributeFullName);
IMemoryPackable = GetTypeByMetadataName("MemoryPack.IMemoryPackable`1").ConstructUnboundGenericType();
KnownTypes = new WellKnownTypes(this);
Expand Down Expand Up @@ -161,7 +163,7 @@ public class WellKnownTypes

{ "System.Collections.Generic.KeyValuePair<,>", "global::MemoryPack.Formatters.KeyValuePairFormatter<TREPLACE>" },
{ "System.Lazy<>", "global::MemoryPack.Formatters.LazyFormatter<TREPLACE>" },

// TupleFormatters
{ "System.Tuple<>", "global::MemoryPack.Formatters.TupleFormatter<TREPLACE>" },
{ "System.Tuple<,>", "global::MemoryPack.Formatters.TupleFormatter<TREPLACE>" },
Expand Down
30 changes: 30 additions & 0 deletions tests/MemoryPack.Tests/DefaultValueTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using MemoryPack.Tests.Models;

namespace MemoryPack.Tests;

public class DefaultValueTest
{
[Fact]
public void SuppressDefaultInitialization()
{
var bin = MemoryPackSerializer.Serialize(new DefaultValuePlaceholder { X = 1 });
var expected = new HasDefaultValue();
var deserializedValue = MemoryPackSerializer.Deserialize<HasDefaultValue>(bin)!;
deserializedValue.Y.Should().Be(default);
deserializedValue.Z.Should().Be(default);
deserializedValue.Y2.Should().Be(expected.Y2);
deserializedValue.Z2.Should().Be(expected.Z2);
}

[Fact]
public void SuppressDefaultInitialization_VersionTolerant()
{
var bin = MemoryPackSerializer.Serialize(new DefaultValuePlaceholderWithVersionTolerant { X = 1 });
var expected = new HasDefaultValueWithVersionTolerant();
var deserializedValue = MemoryPackSerializer.Deserialize<HasDefaultValueWithVersionTolerant>(bin)!;
deserializedValue.Y.Should().Be(default);
deserializedValue.Z.Should().Be(default);
deserializedValue.Y2.Should().Be(expected.Y2);
deserializedValue.Z2.Should().Be(expected.Z2);
}
}
47 changes: 47 additions & 0 deletions tests/MemoryPack.Tests/GeneratorDiagnosticsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,53 @@ public partial class Tester

""");
}

[Fact]
public void MEMPACK040_SuppressDefaultInitializationMustBeSettable()
{
Compile(40, """
using MemoryPack;

[MemoryPackable]
public partial class Tester
{
[SuppressDefaultInitialization]
public required int I1 { get; set; }
}

""");

Compile(40, """
using MemoryPack;

[MemoryPackable]
public partial class Tester
{
[SuppressDefaultInitialization]
public int I1 { get; init; }
}

""");

Compile(40, """
using MemoryPack;

[MemoryPackable]
public partial class Tester
{
[SuppressDefaultInitialization]
public readonly int I1;

[MemoryPackConstructor]
public Tester(int i1)
{
I1 = i1;
}
}

""");

}
}


Expand Down
70 changes: 22 additions & 48 deletions tests/MemoryPack.Tests/Models/DefaultValues.cs
Original file line number Diff line number Diff line change
@@ -1,69 +1,43 @@
using System;
using System.Collections.Generic;

namespace MemoryPack.Tests.Models;

enum TestEnum
{
A, B, C
}

[MemoryPackable]
partial class DefaultValuePlaceholder
{
public int X { get; set; }
}

[MemoryPackable]
partial class FieldDefaultValue
[MemoryPackable(GenerateType.VersionTolerant, SerializeLayout.Sequential)]
partial class DefaultValuePlaceholderWithVersionTolerant
{
public int X;
public int Y = 12345;
public float Z = 678.9f;
public string S = "aaaaaaaaa";
public bool B = true;
public int X { get; set; }
}

[MemoryPackable]
partial class PropertyDefaultValue
[MemoryPackable(GenerateType.VersionTolerant, SerializeLayout.Sequential)]
partial class HasDefaultValueWithVersionTolerant
{
internal enum NestedEnum
{
A, B
}
public int X;

public int X { get; set; }
public int Y { get; set; } = 12345;
public int Y = 12345;
public float Z { get; set; } = 678.9f;
public string S { get; set; } = "aaaaaaaaa";
public bool B { get; set; } = true;
public List<string> Alpha { get; set; } = new List<string>(new HashSet<string>());
public TestEnum E { get; set; } = TestEnum.A;
public NestedEnum E2 { get; set; } = NestedEnum.A;
public (TestEnum, List<string>) Tuple { get; set; } = (TestEnum.A, new List<string>(new HashSet<string>()));
public DateTime Struct { get; set; } = default!;

[SuppressDefaultInitialization]
public int Y2 = 12345;

[SuppressDefaultInitialization]
public float Z2 { get; set; } = 678.9f;
}

[MemoryPackable]
partial class CtorParamDefaultValue
partial class HasDefaultValue
{
public int X;
public int Y;
public float Z;
public string S;
public bool B;
public decimal D;
public DateTime StructValue;

[MemoryPackConstructor]
public CtorParamDefaultValue(int x, int y = 12345, float z = 678.9f, string s = "aaaaaa", bool b = true, decimal d = 99M, DateTime structValue = default)
{
X = x;
Y = y;
Z = z;
S = s;
B = b;
D = d;
StructValue = structValue;
}
public int Y = 12345;
public float Z { get; set; } = 678.9f;

[SuppressDefaultInitialization]
public int Y2 = 12345;

[SuppressDefaultInitialization]
public float Z2 { get; set; } = 678.9f;
}

0 comments on commit 4cefdad

Please sign in to comment.