Skip to content

Commit 442e754

Browse files
committed
Struct-based smart enums and value objects with reference key types, as well as ad hoc unions must be required.
1 parent fdcaf64 commit 442e754

File tree

13 files changed

+245
-49
lines changed

13 files changed

+245
-49
lines changed

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<Copyright>(c) $([System.DateTime]::Now.Year), Pawel Gerr. All rights reserved.</Copyright>
5-
<VersionPrefix>8.6.1</VersionPrefix>
5+
<VersionPrefix>8.7.0</VersionPrefix>
66
<Authors>Pawel Gerr</Authors>
77
<GenerateDocumentationFile>true</GenerateDocumentationFile>
88
<PackageProjectUrl>https://github.com/PawelGerr/Thinktecture.Runtime.Extensions</PackageProjectUrl>

samples/Thinktecture.Runtime.Extensions.Benchmarking/Benchmarks/LoadingSmartEnums.cs

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,19 @@ namespace Thinktecture.Benchmarks;
1010

1111
/*
1212
13-
22.01.2025
13+
15.04.2025
1414
15-
.NET 9.0.1
15+
.NET 9.0.4
16+
17+
| Method | Mean | Error | StdDev | Median | Allocated |
18+
|----------------------------- |---------:|---------:|----------:|----------:|----------:|
19+
| Real_Enum_StringConverter | 11.34 ms | 1.382 ms | 3.876 ms | 9.640 ms | 7.16 MB |
20+
| SmartEnum_Struct_StringBased | 11.63 ms | 1.485 ms | 4.214 ms | 10.541 ms | 8.51 MB |
21+
| SmartEnum_Class_StringBased | 15.93 ms | 2.599 ms | 7.416 ms | 13.270 ms | 8.21 MB |
22+
| Real_Enum_IntBased | 15.07 ms | 2.923 ms | 8.574 ms | 11.595 ms | 6.67 MB |
23+
| SmartEnum_Struct_IntBased | 16.91 ms | 4.670 ms | 13.399 ms | 10.216 ms | 8.02 MB |
24+
| SmartEnum_Class_IntBased | 20.11 ms | 5.255 ms | 15.078 ms | 12.856 ms | 7.72 MB |
1625
17-
| Method | Mean | Error | StdDev | Median | Allocated |
18-
|----------------------------- |----------:|----------:|----------:|----------:|----------:|
19-
| Real_Enum_StringConverter | 15.722 ms | 3.4459 ms | 10.106 ms | 10.545 ms | 7.16 MB |
20-
| SmartEnum_Struct_StringBased | 12.430 ms | 2.6370 ms | 7.307 ms | 10.046 ms | 8.51 MB |
21-
| SmartEnum_Class_StringBased | 9.328 ms | 1.1271 ms | 3.008 ms | 8.682 ms | 8.21 MB |
22-
| Real_Enum_IntBased | 6.801 ms | 1.0520 ms | 2.826 ms | 5.830 ms | 6.67 MB |
23-
| SmartEnum_Struct_IntBased | 7.341 ms | 0.8959 ms | 2.482 ms | 6.327 ms | 8.03 MB |
24-
| SmartEnum_Class_IntBased | 7.453 ms | 0.8701 ms | 2.352 ms | 6.643 ms | 7.72 MB |
2526
2627
*/
2728

@@ -35,12 +36,18 @@ public class LoadingSmartEnums
3536
private const int _NUMBER_OF_ENTITIES = 10_000;
3637
private static readonly RealEnum[] _enums = Enum.GetValues<RealEnum>();
3738

38-
private readonly Entity_Enum_StringConverter[] _Entity_Enum_StringConverter = Enumerable.Range(1, _NUMBER_OF_ENTITIES).Select(i => new Entity_Enum_StringConverter(i, _enums[i % _enums.Length])).ToArray();
39-
private readonly Entity_Enum_IntBased[] _Entity_Enum_IntBased = Enumerable.Range(1, _NUMBER_OF_ENTITIES).Select(i => new Entity_Enum_IntBased(i, _enums[i % _enums.Length])).ToArray();
40-
private readonly Entity_SmartEnum_Class_StringBased[] _Entity_SmartEnum_Class_StringBased = Enumerable.Range(1, _NUMBER_OF_ENTITIES).Select(i => new Entity_SmartEnum_Class_StringBased(i, TestSmartEnum_Class_StringBased.Items[i % _enums.Length])).ToArray();
41-
private readonly Entity_SmartEnum_Struct_StringBased[] _Entity_SmartEnum_Struct_StringBased = Enumerable.Range(1, _NUMBER_OF_ENTITIES).Select(i => new Entity_SmartEnum_Struct_StringBased(i, TestSmartEnum_Struct_StringBased.Items[i % _enums.Length])).ToArray();
42-
private readonly Entity_SmartEnum_Class_IntBased[] _Entity_SmartEnum_Class_IntBased = Enumerable.Range(1, _NUMBER_OF_ENTITIES).Select(i => new Entity_SmartEnum_Class_IntBased(i, TestSmartEnum_Class_IntBased.Items[i % _enums.Length])).ToArray();
43-
private readonly Entity_SmartEnum_Struct_IntBased[] _Entity_SmartEnum_Struct_IntBased = Enumerable.Range(1, _NUMBER_OF_ENTITIES).Select(i => new Entity_SmartEnum_Struct_IntBased(i, TestSmartEnum_Struct_IntBased.Items[i % _enums.Length])).ToArray();
39+
private readonly Entity_Enum_StringConverter[] _Entity_Enum_StringConverter
40+
= Enumerable.Range(1, _NUMBER_OF_ENTITIES).Select(i => new Entity_Enum_StringConverter(i, _enums[i % _enums.Length]) { Enum = RealEnum.Value1 }).ToArray();
41+
private readonly Entity_Enum_IntBased[] _Entity_Enum_IntBased
42+
= Enumerable.Range(1, _NUMBER_OF_ENTITIES).Select(i => new Entity_Enum_IntBased(i, _enums[i % _enums.Length]) { Enum = RealEnum.Value1 }).ToArray();
43+
private readonly Entity_SmartEnum_Class_StringBased[] _Entity_SmartEnum_Class_StringBased
44+
= Enumerable.Range(1, _NUMBER_OF_ENTITIES).Select(i => new Entity_SmartEnum_Class_StringBased(i, TestSmartEnum_Class_StringBased.Items[i % _enums.Length]) { Enum = TestSmartEnum_Class_StringBased.Value1 }).ToArray();
45+
private readonly Entity_SmartEnum_Struct_StringBased[] _Entity_SmartEnum_Struct_StringBased
46+
= Enumerable.Range(1, _NUMBER_OF_ENTITIES).Select(i => new Entity_SmartEnum_Struct_StringBased(i, TestSmartEnum_Struct_StringBased.Items[i % _enums.Length]) { Enum = TestSmartEnum_Struct_StringBased.Value1 }).ToArray();
47+
private readonly Entity_SmartEnum_Class_IntBased[] _Entity_SmartEnum_Class_IntBased
48+
= Enumerable.Range(1, _NUMBER_OF_ENTITIES).Select(i => new Entity_SmartEnum_Class_IntBased(i, TestSmartEnum_Class_IntBased.Items[i % _enums.Length]) { Enum = TestSmartEnum_Class_IntBased.Value1 }).ToArray();
49+
private readonly Entity_SmartEnum_Struct_IntBased[] _Entity_SmartEnum_Struct_IntBased
50+
= Enumerable.Range(1, _NUMBER_OF_ENTITIES).Select(i => new Entity_SmartEnum_Struct_IntBased(i, TestSmartEnum_Struct_IntBased.Items[i % _enums.Length]) { Enum = TestSmartEnum_Struct_IntBased.Value1 }).ToArray();
4451

4552
[GlobalSetup]
4653
public void Initialize()

samples/Thinktecture.Runtime.Extensions.Benchmarking/Benchmarks/LoadingValueObjects.cs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ namespace Thinktecture.Benchmarks;
99

1010
/*
1111
12-
22.01.2025
12+
15.04.2025
1313
14-
.NET 9.0.1
14+
.NET 9.0.4
1515
16-
| Method | Mean | Error | StdDev | Median | Gen0 | Gen1 | Allocated |
17-
|------------------------------- |---------:|--------:|---------:|---------:|----------:|----------:|----------:|
18-
| Entity_with_ValueObjects | 178.4 ms | 7.83 ms | 22.73 ms | 172.4 ms | 4000.0000 | 3000.0000 | 89.13 MB |
19-
| Entity_without_ValueObjects | 175.6 ms | 8.77 ms | 25.84 ms | 166.2 ms | 4000.0000 | 3000.0000 | 84.55 MB |
20-
| Entity_with_StructValueObjects | 164.7 ms | 8.33 ms | 24.30 ms | 157.5 ms | 4000.0000 | 3000.0000 | 92.61 MB |
16+
| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
17+
|------------------------------- |---------:|--------:|---------:|----------:|----------:|----------:|
18+
| Entity_with_ValueObjects | 180.7 ms | 6.46 ms | 18.53 ms | 4000.0000 | 3000.0000 | 84.38 MB |
19+
| Entity_without_ValueObjects | 143.0 ms | 5.76 ms | 16.97 ms | 3000.0000 | 2000.0000 | 79.8 MB |
20+
| Entity_with_StructValueObjects | 134.2 ms | 4.16 ms | 12.26 ms | 3000.0000 | 2000.0000 | 87.86 MB |
2121
2222
*/
2323

@@ -30,9 +30,21 @@ public class LoadingValueObjects
3030

3131
private const int _NUMBER_OF_ENTITIES = 100_000;
3232

33-
private readonly Entity_with_ValueObjects[] _Entity_with_ValueObjects = Enumerable.Range(1, _NUMBER_OF_ENTITIES).Select(i => new Entity_with_ValueObjects(i)).ToArray();
34-
private readonly Entity_without_ValueObjects[] _Entity_without_ValueObjects = Enumerable.Range(1, _NUMBER_OF_ENTITIES).Select(i => new Entity_without_ValueObjects(i)).ToArray();
35-
private readonly Entity_with_StructValueObjects[] _Entity_with_StructValueObjects = Enumerable.Range(1, _NUMBER_OF_ENTITIES).Select(i => new Entity_with_StructValueObjects(i)).ToArray();
33+
private readonly Entity_with_ValueObjects[] _Entity_with_ValueObjects = Enumerable.Range(1, _NUMBER_OF_ENTITIES).Select(i => new Entity_with_ValueObjects(i)
34+
{
35+
Name = Name.Create("Name"),
36+
Description = Description.Create("Description"),
37+
}).ToArray();
38+
private readonly Entity_without_ValueObjects[] _Entity_without_ValueObjects = Enumerable.Range(1, _NUMBER_OF_ENTITIES).Select(i => new Entity_without_ValueObjects(i)
39+
{
40+
Name = "Name",
41+
Description = "Description",
42+
}).ToArray();
43+
private readonly Entity_with_StructValueObjects[] _Entity_with_StructValueObjects = Enumerable.Range(1, _NUMBER_OF_ENTITIES).Select(i => new Entity_with_StructValueObjects(i)
44+
{
45+
Name = NameStruct.Create("Name"),
46+
Description = DescriptionStruct.Create("Description"),
47+
}).ToArray();
3648

3749
[GlobalSetup]
3850
public void Initialize()

samples/Thinktecture.Runtime.Extensions.Benchmarking/Database/Entity_SmartEnum_Struct_StringBased.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ namespace Thinktecture.Database;
44
public class Entity_SmartEnum_Struct_StringBased
55
{
66
public int Id { get; set; }
7-
public TestSmartEnum_Struct_StringBased Enum { get; set; }
7+
public required TestSmartEnum_Struct_StringBased Enum { get; set; }
88

99
public Entity_SmartEnum_Struct_StringBased(int id, TestSmartEnum_Struct_StringBased @enum)
1010
{

samples/Thinktecture.Runtime.Extensions.Benchmarking/Database/Entity_with_StructValueObjects.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ namespace Thinktecture.Database;
44
public class Entity_with_StructValueObjects
55
{
66
public int Id { get; set; }
7-
public NameStruct Name { get; set; }
8-
public DescriptionStruct Description { get; set; }
7+
public required NameStruct Name { get; set; }
8+
public required DescriptionStruct Description { get; set; }
99

1010
// ReSharper disable once UnusedMember.Local
1111
private Entity_with_StructValueObjects(int id, NameStruct name, DescriptionStruct description)

samples/Thinktecture.Runtime.Extensions.EntityFrameworkCore.Samples/Product.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Diagnostics.CodeAnalysis;
23
using Thinktecture.SmartEnums;
34
using Thinktecture.ValueObjects;
45

@@ -11,11 +12,12 @@ public class Product
1112
public ProductCategory Category { get; private set; }
1213
public ProductType ProductType { get; private set; }
1314
public OpenEndDate EndDate { get; set; }
14-
public DayMonth ScheduledDeliveryDate { get; set; }
15+
public required DayMonth ScheduledDeliveryDate { get; set; }
1516

1617
private Boundary? _boundary;
1718
public Boundary Boundary => _boundary ?? throw new InvalidOperationException("Boundary is not loaded.");
1819

20+
[SetsRequiredMembers]
1921
private Product(
2022
Guid id,
2123
ProductName name,
@@ -32,6 +34,7 @@ private Product(
3234
ScheduledDeliveryDate = scheduledDeliveryDate;
3335
}
3436

37+
[SetsRequiredMembers]
3538
public Product(
3639
Guid id,
3740
ProductName name,

samples/Thinktecture.Runtime.Extensions.Samples/ValueObjects/ProductNameStruct.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace Thinktecture.ValueObjects;
44

5-
[ValueObject<string>(DefaultInstancePropertyName = "None")]
5+
[ValueObject<string>]
66
[ValueObjectKeyMemberEqualityComparer<ComparerAccessors.StringOrdinalIgnoreCase, string>]
77
[ValueObjectKeyMemberComparer<ComparerAccessors.StringOrdinalIgnoreCase, string>]
88
public partial struct ProductNameStruct

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/CodeFixes/ThinktectureRuntimeExtensionsCodeFixProvider.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public sealed class ThinktectureRuntimeExtensionsCodeFixProvider : CodeFixProvid
1111
private const string _MAKE_PARTIAL = "Make the type partial";
1212
private const string _MAKE_METHOD_PARTIAL = "Make the method partial";
1313
private const string _MAKE_MEMBER_PUBLIC = "Make the member public";
14+
private const string _MAKE_MEMBER_REQUIRED = "Make the member required";
1415
private const string _MAKE_FIELD_READONLY = "Make the field read-only";
1516
private const string _REMOVE_PROPERTY_SETTER = "Remove property setter";
1617
private const string _MAKE_INIT_PRIVATE = "Make init private";
@@ -39,6 +40,7 @@ public sealed class ThinktectureRuntimeExtensionsCodeFixProvider : CodeFixProvid
3940
DiagnosticsDescriptors.ExplicitEqualityComparerWithoutComparer.Id,
4041
DiagnosticsDescriptors.MethodWithUseDelegateFromConstructorMustBePartial.Id,
4142
DiagnosticsDescriptors.UnionRecordMustBeSealed.Id,
43+
DiagnosticsDescriptors.MembersDisallowingDefaultValuesMustBeRequired.Id,
4244
];
4345

4446
/// <inheritdoc />
@@ -120,6 +122,10 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
120122
{
121123
context.RegisterCodeFix(CodeAction.Create(_SEAL_CLASS, _ => ReplaceOrAddTypeModifierAsync(context.Document, root, GetCodeFixesContext().TypeDeclaration, SyntaxKind.AbstractKeyword, SyntaxKind.SealedKeyword), _SEAL_CLASS), diagnostic);
122124
}
125+
else if (diagnostic.Id == DiagnosticsDescriptors.MembersDisallowingDefaultValuesMustBeRequired.Id)
126+
{
127+
context.RegisterCodeFix(CodeAction.Create(_MAKE_MEMBER_REQUIRED, _ => AddTypeModifierAsync(context.Document, root, GetCodeFixesContext().MemberDeclaration, SyntaxKind.RequiredKeyword), _MAKE_MEMBER_REQUIRED), diagnostic);
128+
}
123129
}
124130
}
125131

@@ -452,6 +458,9 @@ private sealed class CodeFixesContext
452458
private FieldDeclarationSyntax? _fieldDeclaration;
453459
public FieldDeclarationSyntax? FieldDeclaration => _fieldDeclaration ??= GetDeclaration<FieldDeclarationSyntax>();
454460

461+
private MemberDeclarationSyntax? _memberDeclaration;
462+
public MemberDeclarationSyntax? MemberDeclaration => _memberDeclaration ??= GetDeclaration<MemberDeclarationSyntax>();
463+
455464
private PropertyDeclarationSyntax? _propertyDeclaration;
456465
public PropertyDeclarationSyntax? PropertyDeclaration => _propertyDeclaration ??= GetDeclaration<PropertyDeclarationSyntax>();
457466

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Diagnostics/ThinktectureRuntimeExtensionsAnalyzer.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Microsoft.CodeAnalysis.CSharp;
12
using Microsoft.CodeAnalysis.CSharp.Syntax;
23
using Microsoft.CodeAnalysis.Diagnostics;
34
using Microsoft.CodeAnalysis.Operations;
@@ -57,6 +58,7 @@ public sealed class ThinktectureRuntimeExtensionsAnalyzer : DiagnosticAnalyzer
5758
DiagnosticsDescriptors.NonAbstractDerivedUnionIsLessAccessibleThanBaseUnion,
5859
DiagnosticsDescriptors.AllowDefaultStructsCannotBeTrueIfValueObjectIsStructButKeyTypeIsClass,
5960
DiagnosticsDescriptors.AllowDefaultStructsCannotBeTrueIfSomeMembersDisallowDefaultValues,
61+
DiagnosticsDescriptors.MembersDisallowingDefaultValuesMustBeRequired,
6062
];
6163

6264
/// <inheritdoc />
@@ -74,6 +76,59 @@ public override void Initialize(AnalysisContext context)
7476
context.RegisterOperationAction(AnalyzeMethodCall, OperationKind.Invocation);
7577
context.RegisterOperationAction(AnalyzeDefaultValueAssignment, OperationKind.DefaultValue);
7678
context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation);
79+
80+
context.RegisterSyntaxNodeAction(AnalyzeFieldDisallowingDefaultValues, SyntaxKind.FieldDeclaration);
81+
context.RegisterSyntaxNodeAction(AnalyzePropertyDisallowingDefaultValues, SyntaxKind.PropertyDeclaration);
82+
}
83+
84+
private static void AnalyzePropertyDisallowingDefaultValues(SyntaxNodeAnalysisContext context)
85+
{
86+
if (context.Node is not PropertyDeclarationSyntax propertyDeclarationSyntax
87+
|| propertyDeclarationSyntax.ExpressionBody is not null // public MyStruct Member => ...;
88+
|| propertyDeclarationSyntax.Initializer is not null // public MyStruct Member { get; } = ...;
89+
|| context.ContainingSymbol is not IPropertySymbol propertySymbol
90+
|| propertySymbol.SetMethod is null // public MyStruct Member { get; }
91+
|| propertySymbol.IsStatic
92+
|| propertySymbol.DeclaredAccessibility < propertySymbol.ContainingType.DeclaredAccessibility // required members must not be less visible than the containing type
93+
|| propertySymbol.SetMethod.DeclaredAccessibility < propertySymbol.ContainingType.DeclaredAccessibility) // setter of required members must not be less visible than the containing type
94+
return;
95+
96+
MemberDisallowingDefaultValuesMustBeRequired(context, propertyDeclarationSyntax, propertySymbol.Type, "property", propertySymbol.Name);
97+
}
98+
99+
private static void AnalyzeFieldDisallowingDefaultValues(SyntaxNodeAnalysisContext context)
100+
{
101+
if (context.Node is not FieldDeclarationSyntax fieldDeclarationSyntax
102+
|| (fieldDeclarationSyntax.Declaration.Variables.Count == 1 && fieldDeclarationSyntax.Declaration.Variables[0].Initializer is not null) // public MyStruct Member = ...;
103+
|| context.ContainingSymbol is not IFieldSymbol fieldSymbol
104+
|| fieldSymbol.IsStatic
105+
|| fieldSymbol.DeclaredAccessibility < fieldSymbol.ContainingType.DeclaredAccessibility) // required members must not be less visible than the containing type
106+
return;
107+
108+
MemberDisallowingDefaultValuesMustBeRequired(context, fieldDeclarationSyntax, fieldSymbol.Type, "field", fieldSymbol.Name);
109+
}
110+
111+
private static void MemberDisallowingDefaultValuesMustBeRequired(
112+
SyntaxNodeAnalysisContext context,
113+
MemberDeclarationSyntax memberDeclarationSyntax,
114+
ITypeSymbol memberType,
115+
string memberKind,
116+
string memberName)
117+
{
118+
if (memberDeclarationSyntax.Modifiers.Any(SyntaxKind.RequiredKeyword))
119+
return;
120+
121+
if (memberType.SpecialType != SpecialType.None || !memberType.IsValueType)
122+
return;
123+
124+
if (!memberType.Interfaces.Any(i => i.IsIDisallowDefaultValue()))
125+
return;
126+
127+
context.ReportDiagnostic(Diagnostic.Create(
128+
DiagnosticsDescriptors.MembersDisallowingDefaultValuesMustBeRequired,
129+
context.Node.GetLocation(),
130+
memberKind,
131+
memberName));
77132
}
78133

79134
private static void AnalyzeMethodWithUseDelegateFromConstructor(OperationAnalysisContext context)

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiagnosticsDescriptors.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,5 @@ internal static class DiagnosticsDescriptors
4848
public static readonly DiagnosticDescriptor StaticPropertiesAreNotConsideredItems = new("TTRESG101", "Static properties are not considered enumeration items, use a field instead", "The static property '{0}' is not considered a enumeration item, use a field instead", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true);
4949
public static readonly DiagnosticDescriptor ExplicitComparerWithoutEqualityComparer = new("TTRESG102", "The type has a comparer defined but no equality comparer", "The type '{0}' has a comparer defined but no equality comparer", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true);
5050
public static readonly DiagnosticDescriptor ExplicitEqualityComparerWithoutComparer = new("TTRESG103", "The type has an equality comparer defined but no comparer", "The type '{0}' has an equality comparer defined but no comparer", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true);
51+
public static readonly DiagnosticDescriptor MembersDisallowingDefaultValuesMustBeRequired = new("TTRESG104", "The member must be marked as 'required' to ensure proper initialization", "The {0} '{1}' must be marked as 'required' to ensure proper initialization", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true);
5152
}

0 commit comments

Comments
 (0)