Skip to content

Commit 4cefdad

Browse files
authored
Merge pull request #268 from Cysharp/hadashiA/skip-overwrite-by-default
Add attr to not overwrite Member with default value
2 parents aa947b3 + 5cc81fc commit 4cefdad

9 files changed

Lines changed: 159 additions & 52 deletions

File tree

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ public partial class Person3
213213
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.
214214

215215
```csharp
216-
[MemoryPackable]
216+
[MemoryPackable]
217217
public partial class MethodCallSample
218218
{
219219
// method call order is static -> instance
@@ -546,6 +546,28 @@ public partial class VersionCheck
546546

547547
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.
548548

549+
550+
By default, when the old data read to new schema, any members not on the data side are initialized with the `default` literal.
551+
If you want to avoid this and use initial values of field/properties, you can use `[SuppressDefaultInitialization]`.
552+
553+
```cs
554+
[MemoryPackable]
555+
public partial class DefaultValue
556+
{
557+
public string Prop1 { get; set; }
558+
559+
[SuppressDefaultInitialization]
560+
public int Prop2 { get; set; } = 111; // < if old data is missing, set `111`.
561+
562+
public int Prop3 { get; set; } = 222; // < if old data is missing, set `default`.
563+
}
564+
```
565+
566+
`[SuppressDefaultInitialization]` has following disadvantages:
567+
- Cannot be used with readonly, init-only, and required modifier.
568+
=- May not be the best performance due to increased conditional branching. (But it would be negligible.)
569+
570+
549571
The next [Serialization info](#serialization-info) section shows how to check for schema changes, e.g., by CI, to prevent accidents.
550572

551573
When using `GenerateType.VersionTolerant`, it supports full version-tolerant.

src/MemoryPack.Core/Attributes.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,6 @@ public sealed class MemoryPackOnDeserializedAttribute : Attribute
153153
public sealed class GenerateTypeScriptAttribute : Attribute
154154
{
155155
}
156+
157+
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
158+
public sealed class SuppressDefaultInitialization : Attribute;

src/MemoryPack.Generator/DiagnosticDescriptors.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,4 +324,12 @@ internal static class DiagnosticDescriptors
324324
category: Category,
325325
defaultSeverity: DiagnosticSeverity.Error,
326326
isEnabledByDefault: true);
327+
328+
public static readonly DiagnosticDescriptor SuppressDefaultInitializationMustBeSettable = new(
329+
id: "MEMPACK040",
330+
title: "Readonly member cannot specify [SuppressDefaultInitialization]",
331+
messageFormat: "The MemoryPackable object '{0}' member '{1}' has [SuppressDefaultInitialization], it cannot be readonly, init-only and required.",
332+
category: Category,
333+
defaultSeverity: DiagnosticSeverity.Error,
334+
isEnabledByDefault: true);
327335
}

src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -539,14 +539,15 @@ private string EmitDeserializeBody()
539539
540540
SET:
541541
{{(!IsUseEmptyConstructor ? "goto NEW;" : "")}}
542-
{{Members.Where(x => x.Symbol != null).Where(x => x.IsAssignable).Select(x => $" {(IsUseEmptyConstructor ? "" : "// ")}value.@{x.Name} = __{x.Name};").NewLine()}}
542+
{{Members.Where(x => x.IsAssignable).Select(x => $" {(IsUseEmptyConstructor ? "" : "// ")}value.@{x.Name} = __{x.Name};").NewLine()}}
543543
goto READ_END;
544544
545545
NEW:
546546
value = {{EmitConstructor()}}
547547
{
548548
{{EmitDeserializeConstruction(" ")}}
549549
};
550+
{{EmitDeserializeConstructionWithBranching(" ")}}
550551
READ_END:
551552
{{readEndBody}}
552553
""";
@@ -916,10 +917,23 @@ string EmitDeserializeConstruction(string indent)
916917
{
917918
// all value is deserialized, __Name is exsits.
918919
return string.Join("," + Environment.NewLine, Members
919-
.Where(x => x.IsSettable && !x.IsConstructorParameter)
920+
.Where(x => x is { IsSettable: true, IsConstructorParameter: false, SuppressDefaultInitialization: false })
920921
.Select(x => $"{indent}@{x.Name} = __{x.Name}"));
921922
}
922923

924+
string EmitDeserializeConstructionWithBranching(string indent)
925+
{
926+
var members = Members
927+
.Select((x, i) => (x, i))
928+
.Where(v => v.x.SuppressDefaultInitialization);
929+
930+
var lines = GenerateType is GenerateType.VersionTolerant or GenerateType.CircularReference
931+
? members.Select(v => $"{indent}if (deltas.Length > {v.i} && deltas[{v.i}] != 0) value.@{v.x.Name} = __{v.x.Name};")
932+
: members.Select(v => $"{indent}if ({v.i + 1} <= count) value.@{v.x.Name} = __{v.x.Name};");
933+
934+
return lines.NewLine();
935+
}
936+
923937
string EmitUnionTemplate(IGeneratorContext context)
924938
{
925939
var classOrInterfaceOrRecord = IsRecord ? "record" : (Symbol.TypeKind == TypeKind.Interface) ? "interface" : "class";

src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,11 @@ public bool Validate(TypeDeclarationSyntax syntax, IGeneratorContext context, bo
375375
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.ReadOnlyFieldMustBeConstructorMember, item.GetLocation(syntax), Symbol.Name, item.Name));
376376
noError = false;
377377
}
378+
else if (item is { SuppressDefaultInitialization: true, IsAssignable: false })
379+
{
380+
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SuppressDefaultInitializationMustBeSettable, item.GetLocation(syntax), Symbol.Name, item.Name));
381+
noError = false;
382+
}
378383
}
379384
}
380385

@@ -615,6 +620,7 @@ partial class MemberMeta
615620
public int Order { get; }
616621
public bool HasExplicitOrder { get; }
617622
public MemberKind Kind { get; }
623+
public bool SuppressDefaultInitialization { get; }
618624

619625
MemberMeta(int order)
620626
{
@@ -630,6 +636,7 @@ public MemberMeta(ISymbol symbol, IMethodSymbol? constructor, ReferenceSymbols r
630636
this.Symbol = symbol;
631637
this.Name = symbol.Name;
632638
this.Order = sequentialOrder;
639+
this.SuppressDefaultInitialization = symbol.ContainsAttribute(references.SkipOverwriteDefaultAttribute);
633640
var orderAttr = symbol.GetAttribute(references.MemoryPackOrderAttribute);
634641
if (orderAttr != null)
635642
{

src/MemoryPack.Generator/ReferenceSymbols.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public class ReferenceSymbols
2222
public INamedTypeSymbol MemoryPackOnSerializedAttribute { get; }
2323
public INamedTypeSymbol MemoryPackOnDeserializingAttribute { get; }
2424
public INamedTypeSymbol MemoryPackOnDeserializedAttribute { get; }
25+
public INamedTypeSymbol SkipOverwriteDefaultAttribute { get; }
2526
public INamedTypeSymbol GenerateTypeScriptAttribute { get; }
2627
public INamedTypeSymbol IMemoryPackable { get; }
2728

@@ -46,6 +47,7 @@ public ReferenceSymbols(Compilation compilation)
4647
MemoryPackOnSerializedAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackOnSerializedAttribute");
4748
MemoryPackOnDeserializingAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackOnDeserializingAttribute");
4849
MemoryPackOnDeserializedAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackOnDeserializedAttribute");
50+
SkipOverwriteDefaultAttribute = GetTypeByMetadataName("MemoryPack.SuppressDefaultInitialization");
4951
GenerateTypeScriptAttribute = GetTypeByMetadataName(MemoryPackGenerator.GenerateTypeScriptAttributeFullName);
5052
IMemoryPackable = GetTypeByMetadataName("MemoryPack.IMemoryPackable`1").ConstructUnboundGenericType();
5153
KnownTypes = new WellKnownTypes(this);
@@ -161,7 +163,7 @@ public class WellKnownTypes
161163

162164
{ "System.Collections.Generic.KeyValuePair<,>", "global::MemoryPack.Formatters.KeyValuePairFormatter<TREPLACE>" },
163165
{ "System.Lazy<>", "global::MemoryPack.Formatters.LazyFormatter<TREPLACE>" },
164-
166+
165167
// TupleFormatters
166168
{ "System.Tuple<>", "global::MemoryPack.Formatters.TupleFormatter<TREPLACE>" },
167169
{ "System.Tuple<,>", "global::MemoryPack.Formatters.TupleFormatter<TREPLACE>" },
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using MemoryPack.Tests.Models;
2+
3+
namespace MemoryPack.Tests;
4+
5+
public class DefaultValueTest
6+
{
7+
[Fact]
8+
public void SuppressDefaultInitialization()
9+
{
10+
var bin = MemoryPackSerializer.Serialize(new DefaultValuePlaceholder { X = 1 });
11+
var expected = new HasDefaultValue();
12+
var deserializedValue = MemoryPackSerializer.Deserialize<HasDefaultValue>(bin)!;
13+
deserializedValue.Y.Should().Be(default);
14+
deserializedValue.Z.Should().Be(default);
15+
deserializedValue.Y2.Should().Be(expected.Y2);
16+
deserializedValue.Z2.Should().Be(expected.Z2);
17+
}
18+
19+
[Fact]
20+
public void SuppressDefaultInitialization_VersionTolerant()
21+
{
22+
var bin = MemoryPackSerializer.Serialize(new DefaultValuePlaceholderWithVersionTolerant { X = 1 });
23+
var expected = new HasDefaultValueWithVersionTolerant();
24+
var deserializedValue = MemoryPackSerializer.Deserialize<HasDefaultValueWithVersionTolerant>(bin)!;
25+
deserializedValue.Y.Should().Be(default);
26+
deserializedValue.Z.Should().Be(default);
27+
deserializedValue.Y2.Should().Be(expected.Y2);
28+
deserializedValue.Z2.Should().Be(expected.Z2);
29+
}
30+
}

tests/MemoryPack.Tests/GeneratorDiagnosticsTest.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,53 @@ public partial class Tester
642642
643643
""");
644644
}
645+
646+
[Fact]
647+
public void MEMPACK040_SuppressDefaultInitializationMustBeSettable()
648+
{
649+
Compile(40, """
650+
using MemoryPack;
651+
652+
[MemoryPackable]
653+
public partial class Tester
654+
{
655+
[SuppressDefaultInitialization]
656+
public required int I1 { get; set; }
657+
}
658+
659+
""");
660+
661+
Compile(40, """
662+
using MemoryPack;
663+
664+
[MemoryPackable]
665+
public partial class Tester
666+
{
667+
[SuppressDefaultInitialization]
668+
public int I1 { get; init; }
669+
}
670+
671+
""");
672+
673+
Compile(40, """
674+
using MemoryPack;
675+
676+
[MemoryPackable]
677+
public partial class Tester
678+
{
679+
[SuppressDefaultInitialization]
680+
public readonly int I1;
681+
682+
[MemoryPackConstructor]
683+
public Tester(int i1)
684+
{
685+
I1 = i1;
686+
}
687+
}
688+
689+
""");
690+
691+
}
645692
}
646693

647694

Lines changed: 22 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,43 @@
1-
using System;
2-
using System.Collections.Generic;
3-
41
namespace MemoryPack.Tests.Models;
52

6-
enum TestEnum
7-
{
8-
A, B, C
9-
}
10-
113
[MemoryPackable]
124
partial class DefaultValuePlaceholder
135
{
146
public int X { get; set; }
157
}
168

17-
[MemoryPackable]
18-
partial class FieldDefaultValue
9+
[MemoryPackable(GenerateType.VersionTolerant, SerializeLayout.Sequential)]
10+
partial class DefaultValuePlaceholderWithVersionTolerant
1911
{
20-
public int X;
21-
public int Y = 12345;
22-
public float Z = 678.9f;
23-
public string S = "aaaaaaaaa";
24-
public bool B = true;
12+
public int X { get; set; }
2513
}
2614

27-
[MemoryPackable]
28-
partial class PropertyDefaultValue
15+
[MemoryPackable(GenerateType.VersionTolerant, SerializeLayout.Sequential)]
16+
partial class HasDefaultValueWithVersionTolerant
2917
{
30-
internal enum NestedEnum
31-
{
32-
A, B
33-
}
18+
public int X;
3419

35-
public int X { get; set; }
36-
public int Y { get; set; } = 12345;
20+
public int Y = 12345;
3721
public float Z { get; set; } = 678.9f;
38-
public string S { get; set; } = "aaaaaaaaa";
39-
public bool B { get; set; } = true;
40-
public List<string> Alpha { get; set; } = new List<string>(new HashSet<string>());
41-
public TestEnum E { get; set; } = TestEnum.A;
42-
public NestedEnum E2 { get; set; } = NestedEnum.A;
43-
public (TestEnum, List<string>) Tuple { get; set; } = (TestEnum.A, new List<string>(new HashSet<string>()));
44-
public DateTime Struct { get; set; } = default!;
22+
23+
[SuppressDefaultInitialization]
24+
public int Y2 = 12345;
25+
26+
[SuppressDefaultInitialization]
27+
public float Z2 { get; set; } = 678.9f;
4528
}
4629

4730
[MemoryPackable]
48-
partial class CtorParamDefaultValue
31+
partial class HasDefaultValue
4932
{
5033
public int X;
51-
public int Y;
52-
public float Z;
53-
public string S;
54-
public bool B;
55-
public decimal D;
56-
public DateTime StructValue;
5734

58-
[MemoryPackConstructor]
59-
public CtorParamDefaultValue(int x, int y = 12345, float z = 678.9f, string s = "aaaaaa", bool b = true, decimal d = 99M, DateTime structValue = default)
60-
{
61-
X = x;
62-
Y = y;
63-
Z = z;
64-
S = s;
65-
B = b;
66-
D = d;
67-
StructValue = structValue;
68-
}
35+
public int Y = 12345;
36+
public float Z { get; set; } = 678.9f;
37+
38+
[SuppressDefaultInitialization]
39+
public int Y2 = 12345;
40+
41+
[SuppressDefaultInitialization]
42+
public float Z2 { get; set; } = 678.9f;
6943
}

0 commit comments

Comments
 (0)