diff --git a/README.md b/README.md index aa6c0e0..edb4469 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. diff --git a/src/MemoryPack.Core/Attributes.cs b/src/MemoryPack.Core/Attributes.cs index 3e65013..4f3befa 100644 --- a/src/MemoryPack.Core/Attributes.cs +++ b/src/MemoryPack.Core/Attributes.cs @@ -153,3 +153,6 @@ public sealed class MemoryPackOnDeserializedAttribute : Attribute public sealed class GenerateTypeScriptAttribute : Attribute { } + +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public sealed class SuppressDefaultInitialization : Attribute; diff --git a/src/MemoryPack.Generator/DiagnosticDescriptors.cs b/src/MemoryPack.Generator/DiagnosticDescriptors.cs index 9faf9f8..1a572b5 100644 --- a/src/MemoryPack.Generator/DiagnosticDescriptors.cs +++ b/src/MemoryPack.Generator/DiagnosticDescriptors.cs @@ -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); } diff --git a/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs b/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs index 5222832..37a28aa 100644 --- a/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs +++ b/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs @@ -539,7 +539,7 @@ 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: @@ -547,6 +547,7 @@ private string EmitDeserializeBody() { {{EmitDeserializeConstruction(" ")}} }; +{{EmitDeserializeConstructionWithBranching(" ")}} READ_END: {{readEndBody}} """; @@ -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"; diff --git a/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs b/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs index 5e56fdd..3df1694 100644 --- a/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs +++ b/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs @@ -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; + } } } @@ -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) { @@ -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) { diff --git a/src/MemoryPack.Generator/ReferenceSymbols.cs b/src/MemoryPack.Generator/ReferenceSymbols.cs index b1f001a..0015250 100644 --- a/src/MemoryPack.Generator/ReferenceSymbols.cs +++ b/src/MemoryPack.Generator/ReferenceSymbols.cs @@ -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; } @@ -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); @@ -161,7 +163,7 @@ public class WellKnownTypes { "System.Collections.Generic.KeyValuePair<,>", "global::MemoryPack.Formatters.KeyValuePairFormatter" }, { "System.Lazy<>", "global::MemoryPack.Formatters.LazyFormatter" }, - + // TupleFormatters { "System.Tuple<>", "global::MemoryPack.Formatters.TupleFormatter" }, { "System.Tuple<,>", "global::MemoryPack.Formatters.TupleFormatter" }, diff --git a/tests/MemoryPack.Tests/DefaultValueTest.cs b/tests/MemoryPack.Tests/DefaultValueTest.cs new file mode 100644 index 0000000..e9a6334 --- /dev/null +++ b/tests/MemoryPack.Tests/DefaultValueTest.cs @@ -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(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(bin)!; + deserializedValue.Y.Should().Be(default); + deserializedValue.Z.Should().Be(default); + deserializedValue.Y2.Should().Be(expected.Y2); + deserializedValue.Z2.Should().Be(expected.Z2); + } +} diff --git a/tests/MemoryPack.Tests/GeneratorDiagnosticsTest.cs b/tests/MemoryPack.Tests/GeneratorDiagnosticsTest.cs index 492517c..5fed7c1 100644 --- a/tests/MemoryPack.Tests/GeneratorDiagnosticsTest.cs +++ b/tests/MemoryPack.Tests/GeneratorDiagnosticsTest.cs @@ -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; + } +} + +"""); + + } } diff --git a/tests/MemoryPack.Tests/Models/DefaultValues.cs b/tests/MemoryPack.Tests/Models/DefaultValues.cs index c13aa0a..7a1bb42 100644 --- a/tests/MemoryPack.Tests/Models/DefaultValues.cs +++ b/tests/MemoryPack.Tests/Models/DefaultValues.cs @@ -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 Alpha { get; set; } = new List(new HashSet()); - public TestEnum E { get; set; } = TestEnum.A; - public NestedEnum E2 { get; set; } = NestedEnum.A; - public (TestEnum, List) Tuple { get; set; } = (TestEnum.A, new List(new HashSet())); - 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; }