[Prototype] Alternative shape: derive ReadOnlyMemoryStream / WritableMemoryStream from Stream#129913
[Prototype] Alternative shape: derive ReadOnlyMemoryStream / WritableMemoryStream from Stream#129913ViveliDuCh wants to merge 1 commit into
Conversation
|
Tagging subscribers to this area: @dotnet/area-system-io |
There was a problem hiding this comment.
Pull request overview
This PR prototypes an alternative implementation of ReadOnlyMemoryStream and WritableMemoryStream by deriving them from Stream (with local backing state) instead of MemoryStream, and reverts the private protected field promotions on MemoryStream that were previously added to support a MemoryStream-based wrapper shape.
Changes:
ReadOnlyMemoryStream/WritableMemoryStream: base type changed toStream, with added local state (_position,_lengthwhere applicable,_isOpen, cached read task) and explicitStreamoverrides (seek/position/length/flush/etc.).MemoryStream: revertsprivate protectedfield visibility back toprivate.ref/System.Runtime.cs: updates the public API contract to match the new base type and member declarations.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
| src/libraries/System.Runtime/ref/System.Runtime.cs | Updates the public contract to reflect ReadOnlyMemoryStream / WritableMemoryStream : Stream and their members. |
| src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs | Re-implements ReadOnlyMemoryStream as a Stream-derived type with its own position/open-state and required overrides. |
| src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs | Re-implements WritableMemoryStream as a Stream-derived type with its own length/position/open-state and required overrides. |
| src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs | Reverts promoted field visibility (private protected → private). |
| set | ||
| { | ||
| ArgumentOutOfRangeException.ThrowIfNegative(value); | ||
| ArgumentOutOfRangeException.ThrowIfGreaterThan(value, int.MaxValue); | ||
| EnsureNotClosed(); | ||
| _position = (int)value; | ||
| } |
| { | ||
| EnsureNotClosed(); | ||
|
|
||
| ArgumentOutOfRangeException.ThrowIfGreaterThan(offset, int.MaxValue); |
| /// <inheritdoc/> | ||
| public override bool TryGetBuffer(out ArraySegment<byte> buffer) | ||
| public override void SetLength(long value) => | ||
| throw new NotSupportedException(SR.NotSupported_UnwritableStream); |
| /// <inheritdoc/> | ||
| public override void Write(byte[] buffer, int offset, int count) | ||
| { | ||
| ValidateBufferArguments(buffer, offset, count); | ||
| throw new NotSupportedException(SR.NotSupported_UnwritableStream); | ||
| } |
| public override void Write(ReadOnlySpan<byte> buffer) => | ||
| throw new NotSupportedException(SR.NotSupported_UnwritableStream); | ||
|
|
||
| /// <inheritdoc/> | ||
| public override void WriteByte(byte value) => | ||
| throw new NotSupportedException(SR.NotSupported_UnwritableStream); |
| set | ||
| { | ||
| ArgumentOutOfRangeException.ThrowIfNegative(value); | ||
| ArgumentOutOfRangeException.ThrowIfGreaterThan(value, int.MaxValue); | ||
| EnsureNotClosed(); | ||
| _position = (int)value; | ||
| } |
| { | ||
| EnsureNotClosed(); | ||
|
|
||
| ArgumentOutOfRangeException.ThrowIfGreaterThan(offset, int.MaxValue); |
| @@ -238,8 +313,9 @@ public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationTo | |||
| /// <inheritdoc/> | |||
| public override void SetLength(long value) => throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); | |||
| public sealed partial class ReadOnlyMemoryStream : System.IO.Stream | ||
| { |
|
|
||
| /// <summary>Always throws; the underlying buffer is not exposed.</summary> | ||
| /// <exception cref="UnauthorizedAccessException">Always thrown.</exception> | ||
| public byte[] GetBuffer() => |
There was a problem hiding this comment.
The methods that expose raw buffer as a byte array should most likely be deleted if we want to inherit from Stream. They will just always keep throwing and pollute the API surface. Instead we should consider introducing a method that returns ReadOnlyMemory<byte>
Follow-up on #126669. Shows what
ReadOnlyMemoryStream/WritableMemoryStreamwould look like derived fromStreaminstead ofMemoryStream.Context of the trade-off (recap of the prior discussion)
Streambase.is MemoryStreamfor fast paths; deriving fromMemoryStreamlets the new wrappers participate in those optimizations and keeps naming consistency.MemoryStreamfor the initial shape, withprivate protectedfield promotions onMemoryStreamso the wrappers can share state — the shape that shipped.This PR is the other side of that fork, in case it's ever useful to compare.
What this prototype does
MemoryStream.csprivate protectedfield promotions added in #126669 — no consumer in the hierarchy under this shape.ReadOnlyMemoryStream.cs: MemoryStream→: Stream. Adds local backing state andStreamoverrides (CanRead/CanSeek/CanWrite/Length/Position/Seek/Flush/FlushAsync/SetLength).Capacity,GetBuffer,TryGetBuffer,ToArray,WriteToloseoverride;Capacitysetter dropped (only threw).Write/Write(ROSpan)/WriteBytethrowNotSupportedException.WritableMemoryStream.cs_lengthand the writable surface.ref/System.Runtime.csTrade-offs
For a
StreambaseMemoryStream's buffer-management state (_buffer,_origin,_capacity,_expandable,_exposable), so the inherited fields are dead weight on every instance.MemoryStreambase also brings inherited surface (WriteTo(byte[]), expandable-capacity semantics,GetBuffer/TryGetBufferover an internalbyte[], etc.) that doesn't really apply to a fixedMemory<byte>wrapper — under the current shape these have to be overridden to throw or no-op.Against a
Streambaseis MemoryStreamfast paths used by several ecosystem consumers (mono, EFCore, MSBuild and friends).…MemoryStream, which is a soft signal to users (and to the framework design "self-documenting names" guidance) that they areMemoryStreams. Renaming is on the table in principle, but if the names stay then a non-MemoryStreambase is a mismatch worth flagging.Testing
WritableMemoryStreamTestsReadOnlyMemoryStreamTestsWritableMemoryStreamConformanceTestsReadOnlyMemoryStreamConformanceTestsSystem.IO.TestsSystem.IO.UnmanagedMemoryStream.TestsSeekkeepsMemoryStream'sIOException(SR.IO_SeekBeforeBegin)on negative target to preserve the existing conformance contract.Note
This PR description was drafted with the assistance of GitHub Copilot.