Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/release-notes/.FSharp.Core/10.0.300.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
### Fixed

* `Seq.empty` now uses `System.Linq.Enumerable.Empty<'T>()` instead of an internal DU, fixing serialization issues with JSON.NET, STJ, and ASP.NET ObjectResult. ([Issue #17864](https://github.com/dotnet/fsharp/issues/17864), [PR #19322](https://github.com/dotnet/fsharp/pull/19322))

### Added

### Changed
Expand Down
2 changes: 1 addition & 1 deletion src/FSharp.Core/seq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,7 @@ module Seq =
mkUnfoldSeq generator state

[<CompiledName("Empty")>]
let empty<'T> = (EmptyEnumerable :> seq<'T>)
let empty<'T> = System.Linq.Enumerable.Empty<'T>()

[<CompiledName("InitializeInfinite")>]
let initInfinite initializer =
Expand Down
25 changes: 4 additions & 21 deletions src/FSharp.Core/seqcore.fs
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,6 @@ module internal IEnumerator =

let Empty<'T> () = (new EmptyEnumerator<'T>() :> IEnumerator<'T>)

[<NoEquality; NoComparison>]
type EmptyEnumerable<'T> =

| EmptyEnumerable

interface IEnumerable<'T> with
member _.GetEnumerator() = Empty<'T>()

interface IEnumerable with
member _.GetEnumerator() = (Empty<'T>() :> IEnumerator)

type GeneratedEnumerable<'T, 'State>(openf: unit -> 'State, compute: 'State -> 'T option, closef: 'State -> unit) =
let mutable started = false
Expand Down Expand Up @@ -313,17 +303,10 @@ module RuntimeHelpers =
let rec takeOuter() =
if outerEnum.MoveNext() then
let ie = outerEnum.Current
// Optimization to detect the statically-allocated empty IEnumerables
match box ie with
| :? EmptyEnumerable<'T> ->
// This one is empty, just skip, don't call GetEnumerator, try again
takeOuter()
| _ ->
// OK, this one may not be empty.
// Don't forget to dispose of the enumerator for the inner list now we're done with it
currInnerEnum.Dispose()
currInnerEnum <- ie.GetEnumerator()
takeInner ()
// Don't forget to dispose of the enumerator for the inner list now we're done with it
currInnerEnum.Dispose()
currInnerEnum <- ie.GetEnumerator()
takeInner ()
else
// We're done
x.Finish()
Expand Down
6 changes: 0 additions & 6 deletions src/FSharp.Core/seqcore.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,6 @@ module internal IEnumerator =

val Empty: unit -> IEnumerator<'T>

[<NoEqualityAttribute; NoComparisonAttribute>]
type EmptyEnumerable<'T> =
| EmptyEnumerable

interface IEnumerable
interface IEnumerable<'T>

[<SealedAttribute>]
type Singleton<'T> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2066,4 +2066,16 @@ type SeqModule2() =
// empty list & out of bounds
Assert.AreEqual([0; 0], Seq.insertManyAt 0 [0; 0] [] |> Seq.toList)
CheckThrowsArgumentException (fun () -> Seq.insertManyAt -1 [0; 0] [1] |> Seq.toList |> ignore)
CheckThrowsArgumentException (fun () -> Seq.insertManyAt 2 [0; 0] [1] |> Seq.toList |> ignore)
CheckThrowsArgumentException (fun () -> Seq.insertManyAt 2 [0; 0] [1] |> Seq.toList |> ignore)

[<Fact>]
member _.EmptySerializesAsEmptyArray() =
let json = System.Text.Json.JsonSerializer.Serialize(Seq.empty<int>)
Assert.AreEqual("[]", json)

[<Fact>]
member _.EmptyFormatsCorrectly() =
// Seq.empty uses Enumerable.Empty which returns a cached empty array,
// so %A formats it with array notation
let formatted = sprintf "%A" Seq.empty<int>
Assert.AreEqual("[||]", formatted)