Skip to content

Commit 6091e3c

Browse files
authored
Make partial interface members virtual (#77379)
* Make partial interface members virtual * Simplify code * Document the breaking change * Add more tests * Gate on lang version * Improve code and doc * Improve wording * Update based on LDM * Partial methods were always implicitly private * Revert an unrelated change Likely caused by some unintentional git operation. * Fix wording in doc example * Clarify workaround
1 parent 788461b commit 6091e3c

File tree

9 files changed

+1058
-18
lines changed

9 files changed

+1058
-18
lines changed

docs/compilers/CSharp/Compiler Breaking Changes - DotNet 10.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,3 +364,41 @@ class extension { } // type may not be named "extension"
364364
class C<extension> { } // type parameter may not be named "extension"
365365
```
366366

367+
## Partial properties and events are now implicitly virtual and public
368+
369+
***Introduced in Visual Studio 2022 version 17.15***
370+
371+
We have fixed [an inconsistency](https://github.com/dotnet/roslyn/issues/77346)
372+
where partial interface properties and events would not be implicitly `virtual` and `public` unlike their non-partial equivalents.
373+
This inconsistency is however [preserved](./Deviations%20from%20Standard.md#interface-partial-methods) for partial interface methods to avoid a larger breaking change.
374+
Note that Visual Basic and other languages not supporting default interface members will start requiring to implement implicitly virtual `partial` interface members.
375+
376+
To keep the previous behavior, explicitly mark `partial` interface members as `private` (if they don't have any accessibility modifiers)
377+
and `sealed` (if they don't have the `private` modifier which implies `sealed`, and they don't already have modifier `virtual` or `sealed`).
378+
379+
```cs
380+
System.Console.Write(((I)new C()).P); // wrote 1 previously, writes 2 now
381+
382+
partial interface I
383+
{
384+
public partial int P { get; }
385+
public partial int P => 1; // implicitly virtual now
386+
}
387+
388+
class C : I
389+
{
390+
public int P => 2; // implements I.P
391+
}
392+
```
393+
394+
```cs
395+
System.Console.Write(((I)new C()).P); // inaccessible previously, writes 1 now
396+
397+
partial interface I
398+
{
399+
partial int P { get; } // implicitly public now
400+
partial int P => 1;
401+
}
402+
403+
class C : I;
404+
```

docs/compilers/CSharp/Deviations from Standard.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,10 @@ The compiler is free to make assumptions about the shape and behavior of well-kn
7777
It may not check for unexpected constraints, `Obsolete` attribute, or `UnmanagedCallersOnly` attribute.
7878
It may perform some optimizations based on expectations that the types/members are well-behaved.
7979
Note: the compiler should remain resilient to missing well-known types/members.
80+
81+
# Interface partial methods
82+
83+
Interface partial methods are implicitly non-virtual,
84+
unlike non-partial interface methods and other interface partial member kinds,
85+
see [a related breaking change](./Compiler%20Breaking%20Changes%20-%20DotNet%2010.md#partial-properties-and-events-are-now-implicitly-virtual-and-public)
86+
and [LDM 2025-04-07](https://github.com/dotnet/csharplang/blob/main/meetings/2025/LDM-2025-04-07.md#breaking-change-discussion-making-partial-members-in-interfaces-virtual-andor-public).

src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,24 @@ internal static void CheckFeatureAvailabilityForPartialEventsAndConstructors(Loc
245245
}
246246
#nullable disable
247247

248-
internal static DeclarationModifiers AdjustModifiersForAnInterfaceMember(DeclarationModifiers mods, bool hasBody, bool isExplicitInterfaceImplementation)
248+
internal static DeclarationModifiers AdjustModifiersForAnInterfaceMember(DeclarationModifiers mods, bool hasBody, bool isExplicitInterfaceImplementation, bool forMethod)
249249
{
250+
// Interface partial non-method members are implicitly public and virtual just like their non-partial counterparts.
251+
// Interface partial methods are implicitly private and not virtual (this is a spec violation but being preserved to avoid breaks).
252+
bool notPartialOrNewPartialBehavior = (mods & DeclarationModifiers.Partial) == 0 || !forMethod;
253+
254+
if ((mods & DeclarationModifiers.AccessibilityMask) == 0)
255+
{
256+
if (!isExplicitInterfaceImplementation && notPartialOrNewPartialBehavior)
257+
{
258+
mods |= DeclarationModifiers.Public;
259+
}
260+
else
261+
{
262+
mods |= DeclarationModifiers.Private;
263+
}
264+
}
265+
250266
if (isExplicitInterfaceImplementation)
251267
{
252268
if ((mods & DeclarationModifiers.Abstract) != 0)
@@ -258,11 +274,11 @@ internal static DeclarationModifiers AdjustModifiersForAnInterfaceMember(Declara
258274
{
259275
mods &= ~DeclarationModifiers.Sealed;
260276
}
261-
else if ((mods & (DeclarationModifiers.Private | DeclarationModifiers.Partial | DeclarationModifiers.Virtual | DeclarationModifiers.Abstract)) == 0)
277+
else if ((mods & (DeclarationModifiers.Private | DeclarationModifiers.Virtual | DeclarationModifiers.Abstract)) == 0 && notPartialOrNewPartialBehavior)
262278
{
263279
Debug.Assert(!isExplicitInterfaceImplementation);
264280

265-
if (hasBody || (mods & (DeclarationModifiers.Extern | DeclarationModifiers.Sealed)) != 0)
281+
if (hasBody || (mods & (DeclarationModifiers.Extern | DeclarationModifiers.Partial | DeclarationModifiers.Sealed)) != 0)
266282
{
267283
if ((mods & DeclarationModifiers.Sealed) == 0)
268284
{
@@ -279,18 +295,6 @@ internal static DeclarationModifiers AdjustModifiersForAnInterfaceMember(Declara
279295
}
280296
}
281297

282-
if ((mods & DeclarationModifiers.AccessibilityMask) == 0)
283-
{
284-
if ((mods & DeclarationModifiers.Partial) == 0 && !isExplicitInterfaceImplementation)
285-
{
286-
mods |= DeclarationModifiers.Public;
287-
}
288-
else
289-
{
290-
mods |= DeclarationModifiers.Private;
291-
}
292-
}
293-
294298
return mods;
295299
}
296300

src/Compilers/CSharp/Portable/Symbols/Source/SourceEventSymbol.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ private DeclarationModifiers MakeModifiers(SyntaxTokenList modifiers, bool expli
569569
// Proper errors must have been reported by now.
570570
if (isInterface)
571571
{
572-
mods = ModifierUtils.AdjustModifiersForAnInterfaceMember(mods, !isFieldLike, explicitInterfaceImplementation);
572+
mods = ModifierUtils.AdjustModifiersForAnInterfaceMember(mods, !isFieldLike, explicitInterfaceImplementation, forMethod: false);
573573
}
574574

575575
return mods;

src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbol.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -797,7 +797,8 @@ private static DeclarationModifiers AddImpliedModifiers(DeclarationModifiers mod
797797
if (containingTypeIsInterface)
798798
{
799799
mods = ModifierUtils.AdjustModifiersForAnInterfaceMember(mods, hasBody,
800-
methodKind == MethodKind.ExplicitInterfaceImplementation);
800+
methodKind == MethodKind.ExplicitInterfaceImplementation,
801+
forMethod: true);
801802
}
802803
else if (methodKind == MethodKind.ExplicitInterfaceImplementation)
803804
{

src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,7 @@ private static (DeclarationModifiers modifiers, bool hasExplicitAccessMod) MakeM
482482
// Proper errors must have been reported by now.
483483
if (isInterface)
484484
{
485-
mods = ModifierUtils.AdjustModifiersForAnInterfaceMember(mods, accessorsHaveImplementation, isExplicitInterfaceImplementation);
485+
mods = ModifierUtils.AdjustModifiersForAnInterfaceMember(mods, accessorsHaveImplementation, isExplicitInterfaceImplementation, forMethod: false);
486486
}
487487

488488
if (isIndexer)

0 commit comments

Comments
 (0)