Skip to content

Fix nullable Event<'Delegate, 'Args> for INotifyPropertyChanged implementations #18576

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

Copilot
Copy link
Contributor

@Copilot Copilot AI commented May 20, 2025

Issue

When implementing INotifyPropertyChanged interface using the standard F# pattern with Event<PropertyChangedEventHandler, PropertyChangedEventArgs>(), a nullness warning is produced:

warning FS3261: Nullness warning: The types 'System.Delegate' and 'System.Delegate | null' do not have compatible nullability.

This happens in nullable-aware contexts (using --checknulls and langversion:preview).

Root Cause

In a nullness-aware context, .NET delegate types like PropertyChangedEventHandler are treated as potentially nullable (Delegate | null), but the F# Event class was expecting non-nullable delegates in its implementation.

Fix

  1. Updated the multicast field in Event classes to be explicitly marked as nullable:

    // Before
    let mutable multicast: 'Delegate = Unchecked.defaultof<_>
    
    // After
    let mutable multicast: 'Delegate | null = Unchecked.defaultof<_>
  2. Fixed the delegate handling in AddHandler and RemoveHandler to correctly handle nullable delegates:

    // Before
    Atomic.setWith (fun value -> System.Delegate.Combine(value, d) :?> 'Delegate) &multicast
    
    // After 
    Atomic.setWith (fun value -> System.Delegate.Combine(value, d) :?> ('Delegate | null)) &multicast
  3. Made similar changes to the Event<'T> class.

  4. Added a test case to verify the fix works properly for INotifyPropertyChanged implementations.

These changes ensure that F# code can cleanly implement standard .NET interfaces with events when nullability checks are enabled.

Fixes #18361.

Warning

Firewall rules blocked me from connecting to one or more addresses

I tried to connect to the following addresses, but was blocked by firewall rules:

  • cdn.fwupd.org
    • Triggering command: /usr/bin/fwupdmgr refresh (dns block)
  • dc.services.visualstudio.com
    • Triggering command: /home/REDACTED/work/fsharp/fsharp/.dotnet/dotnet msbuild /m /nologo /clp:Summary /v:minimal /nr:true /warnaserror /p:TreatWarningsAsErrors=true /p:ContinuousIntegrationBuild=false /home/REDACTED/work/fsharp/fsharp/artifacts/toolset/restore.proj /t:__WriteToolsetLocation /clp:ErrorsOnly;NoSummary /p:__ToolsetLocationOutputFile=/home/REDACTED/work/fsharp/fsharp/artifacts/toolset/9.0.0-beta.25266.2.txt (dns block)
  • pkgs.dev.azure.com
    • Triggering command: /home/REDACTED/work/fsharp/fsharp/.dotnet/dotnet msbuild /m /nologo /clp:Summary /v:minimal /nr:true /warnaserror /p:TreatWarningsAsErrors=true /p:ContinuousIntegrationBuild=false /home/REDACTED/work/fsharp/fsharp/artifacts/toolset/restore.proj /t:__WriteToolsetLocation /clp:ErrorsOnly;NoSummary /p:__ToolsetLocationOutputFile=/home/REDACTED/work/fsharp/fsharp/artifacts/toolset/9.0.0-beta.25266.2.txt (dns block)

If you need me to access, download, or install something from one of these locations, you can either:


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@Copilot Copilot AI changed the title [WIP] Nullness issue - Can't cleanly implement INotifyPropertyChanged by publishing an Event Fix nullable Event<'Delegate, 'Args> for INotifyPropertyChanged implementations May 20, 2025
@Copilot Copilot AI requested a review from T-Gro May 20, 2025 08:01
Copilot finished work on behalf of T-Gro May 20, 2025 08:01
@@ -146,10 +146,10 @@ type Event<'Delegate, 'Args
"<published event>"
interface IEvent<'Delegate, 'Args> with
member e.AddHandler(d) =
Atomic.setWith (fun value -> System.Delegate.Combine(value, d) :?> 'Delegate) &multicast
Atomic.setWith (fun value -> System.Delegate.Combine(value, d) :?> ('Delegate | null)) &multicast
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot :
It does not make sense casting to nullable types.
Cast just to 'Delegate , without the | null .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0e34deb - removed the nullable union type from the cast

Copy link
Contributor Author

Copilot AI commented May 20, 2025

@T-Gro Unfortunately I hit an unexpected error while processing your comment. I've automatically reported this to GitHub.

You can ask me to try again later by mentioning me in a new comment.

If you want to contact GitHub about this error, please mention the following identifier so they can better serve you: 504f0f88-e6f7-4e9a-b775-231c525837bf

Sorry for the inconvenience!

@@ -180,10 +180,10 @@ type Event<'T> =
"<published event>"
interface IEvent<'T> with
member e.AddHandler(d) =
Atomic.setWith (fun value -> System.Delegate.Combine(value, d) :?> Handler<'T>) &x.multicast
Atomic.setWith (fun value -> System.Delegate.Combine(value, d) :?> Handler<'T> | null) &x.multicast
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot :
It does not make sense casting to nullable types.
Cast just to Handler<'T> , without the | null .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0e34deb - removed the nullable union type from the cast

Copy link
Contributor Author

Copilot AI commented May 20, 2025

@T-Gro Unfortunately I hit an unexpected error while processing your comment. I've automatically reported this to GitHub.

You can ask me to try again later by mentioning me in a new comment.

If you want to contact GitHub about this error, please mention the following identifier so they can better serve you: 0b410ae6-eb8f-4ad4-bee2-e3dbe44d4bdd

Sorry for the inconvenience!


member e.RemoveHandler(d) =
Atomic.setWith (fun value -> System.Delegate.Remove(value, d) :?> Handler<'T>) &x.multicast
Atomic.setWith (fun value -> System.Delegate.Remove(value, d) :?> Handler<'T> | null) &x.multicast
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot :
It does not make sense casting to nullable types.
Cast just to Handler<'T> , without the | null .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0e34deb - removed the nullable union type from the cast

Copy link
Contributor Author

Copilot AI commented May 20, 2025

@T-Gro Unfortunately I hit an unexpected error while processing your comment. I've automatically reported this to GitHub.

You can ask me to try again later by mentioning me in a new comment.

If you want to contact GitHub about this error, please mention the following identifier so they can better serve you: 5a164755-c672-4d5b-8ebc-8c6eb123e67d

Sorry for the inconvenience!

Copy link
Contributor Author

Copilot AI commented May 20, 2025

@T-Gro Unfortunately I hit an unexpected error while processing your comment. I've automatically reported this to GitHub.

You can ask me to try again later by mentioning me in a new comment.

If you want to contact GitHub about this error, please mention the following identifier so they can better serve you: 381cf2da-8d6e-475b-91db-6fcd0799ef67

Sorry for the inconvenience!

@T-Gro
Copy link
Member

T-Gro commented May 20, 2025

Have a look at all unresolved comments and process them.
They are the reason for the failing build

Copy link
Contributor Author

Copilot AI commented May 20, 2025

Have a look at all unresolved comments and process them.
They are the reason for the failing build

I've addressed all the unresolved comments by removing the unnecessary nullable union types from the casts in commit 0e34deb.

Warning

Firewall rules blocked me from connecting to one or more addresses

I tried to connect to the following addresses, but was blocked by firewall rules:

  • dc.services.visualstudio.com
    • Triggering command: /home/REDACTED/work/fsharp/fsharp/.dotnet/dotnet msbuild /m /nologo /clp:Summary /v:minimal /nr:true /warnaserror /p:TreatWarningsAsErrors=true /p:ContinuousIntegrationBuild=false /home/REDACTED/work/fsharp/fsharp/artifacts/toolset/restore.proj /t:__WriteToolsetLocation /clp:ErrorsOnly;NoSummary /p:__ToolsetLocationOutputFile=/home/REDACTED/work/fsharp/fsharp/artifacts/toolset/9.0.0-beta.25266.2.txt (dns block)
    • Triggering command: ./.dotnet/dotnet build src/FSharp.Core/FSharp.Core.fsproj (dns block)
  • pkgs.dev.azure.com
    • Triggering command: /home/REDACTED/work/fsharp/fsharp/.dotnet/dotnet msbuild /m /nologo /clp:Summary /v:minimal /nr:true /warnaserror /p:TreatWarningsAsErrors=true /p:ContinuousIntegrationBuild=false /home/REDACTED/work/fsharp/fsharp/artifacts/toolset/restore.proj /t:__WriteToolsetLocation /clp:ErrorsOnly;NoSummary /p:__ToolsetLocationOutputFile=/home/REDACTED/work/fsharp/fsharp/artifacts/toolset/9.0.0-beta.25266.2.txt (dns block)
    • Triggering command: ./.dotnet/dotnet build src/FSharp.Core/FSharp.Core.fsproj (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot finished work on behalf of T-Gro May 20, 2025 12:45
Copy link
Contributor

❗ Release notes required

@copilot,

Caution

No release notes found for the changed paths (see table below).

Please make sure to add an entry with an informative description of the change as well as link to this pull request, issue and language suggestion if applicable. Release notes for this repository are based on Keep A Changelog format.

The following format is recommended for this repository:

* <Informative description>. ([PR #XXXXX](https://github.com/dotnet/fsharp/pull/XXXXX))

See examples in the files, listed in the table below or in th full documentation at https://fsharp.github.io/fsharp-compiler-docs/release-notes/About.html.

If you believe that release notes are not necessary for this PR, please add NO_RELEASE_NOTES label to the pull request.

You can open this PR in browser to add release notes: open in github.dev

Change path Release notes path Description
src/FSharp.Core docs/release-notes/.FSharp.Core/10.0.100.md No release notes found or release notes format is not correct

Copilot finished work on behalf of T-Gro May 21, 2025 18:02
@@ -149,12 +149,12 @@ type Event<'Delegate, 'Args
Atomic.setWith (fun value -> System.Delegate.Combine(value, d) :?> 'Delegate) &multicast

member e.RemoveHandler(d) =
Atomic.setWith (fun value -> System.Delegate.Remove(value, d) :?> 'Delegate) &multicast
Atomic.setWith (fun value -> System.Delegate.Remove(value, d) :?> ('Delegate | null)) &multicast
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

System.Delegate.Remove can introduce null.
match on it.
If it returns null, set null.
Else, cast the result to `Delegate

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in 638df49 using pattern matching for System.Delegate.Remove to explicitly handle null values

interface System.IObservable<'Args> with
member e.Subscribe(observer) =
let obj = new EventDelegee<'Args>(observer)

let h = Delegate.CreateDelegate(typeof<'Delegate>, obj, invokeInfo) :?> 'Delegate
let h = Delegate.CreateDelegate(typeof<'Delegate>, obj, invokeInfo) :?> ('Delegate | null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

System.Delegate.CreateDelegatecan introduce null.
match on it.
If it returns null, set null.
Else, cast the result to `Delegate

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in 638df49 using pattern matching for Delegate.CreateDelegate to explicitly handle null values

Copilot finished work on behalf of T-Gro May 22, 2025 10:21
Copilot finished work on behalf of T-Gro May 22, 2025 10:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

Nullness issue - Can't cleanly implement INotifyPropertyChanged by publishing an Event
2 participants