15. Collections, enumeration and consumption
Collection literals create values. Enumeration observes elements according to the source protocol. Consumption acquires items destructively or through a forward-only protocol with an explicit notion of completion.
15.1 Literals, kinds and element types
CS-COLLECTION-001. List and map literals use brackets. Items or entries may be separated by commas or newlines, with a trailing comma allowed. Map entries use key: value. A literal cannot mix list items and map entries. Bare empty brackets with no target infer List <of Object>; no special empty-map delimiter is defined.
let amounts = [150, 250, 400]
let labels = ["MK": "North Macedonia", "DE": "Germany"]
let pending as List <of Customer> = []
CS-COLLECTION-002. A required target determines the collection kind and element/key/value context while the value is a literal. Without a target, inference considers numeric promotion, then a most-specific useful common compatible reference/base/interface type, then Object. Equally plausible common interfaces also fall back to Object rather than arbitrary selection. Maps infer keys and values independently. Nested target context propagates inward.
Fractional literal defaults follow chapter 4, so [1, 2.5, 4] uses the current Decimal inference/promotion rather than an older Double default. A literal value must fit its target numeric type. A noncontextualized empty list remains Object-valued after later Add calls; it is not dynamically reinferred.
CS-COLLECTION-003. Nothing does not compete as an independent element type when other elements provide useful information. Reference elements can retain absence; value elements with nothing infer an absence-capable element type. A nonnullable value target rejects nothing. [nothing] without other context infers List <of Object>.
CS-COLLECTION-004. Statically known duplicate map keys are errors. String-keyed maps use normal case-insensitive key equality, so A and a collide. Runtime collisions have ordinary dictionary insertion failure; last-value-wins is not implicit. Nothing keys are invalid, statically when known and through ordinary insertion failure when dynamic. String-key comparer semantics are not selectively applied to Object-keyed maps. Set targets collapse duplicates; lists preserve them.
CS-COLLECTION-005. Items evaluate left-to-right once; map keys precede their values, with entries in source order. A failure stops construction before later expressions and no partially constructed collection is assigned to the target. Every literal evaluation creates a fresh collection, including empty literals. Lists preserve authored order; map/set enumeration order is not a language guarantee.
CS-COLLECTION-006. A materialized collection keeps its kind. Passing an existing List to a Set-required parameter does not perform an implicit conversion. Target-typed overloads requiring equally good different collection kinds are ambiguous. Nested lists are not automatically flattened or promoted to matrices because lengths happen to match. Collection literals are runtime values, not compile-time constants.
15.2 Required indexed access
CS-INDEX-001. Ordinary indexing requires an existing receiver and a valid index/key. Successful access returns the stored value, including a stored nothing. An absent receiver has ordinary null-reference failure, an invalid sequence/String index has normal index/range failure and a missing dictionary key has normal key-not-found failure. Exact exception subclasses follow the bound CLR contract.
let first = items[0]
let customer = customers[id]
let character = text[5]
A type without a compatible indexer fails binding. A missing key in a valid dictionary is a runtime lookup failure. The compiler must not replace failed required access with nothing or the element default.
CS-INDEX-002. Getter/indexer exceptions propagate. Null-safe property navigation does not imply silent method calls, writes or indexed access.
CS-INDEX-003. try_get receiver[index] applies the exception-to-nothing acquisition policy. It covers absent receivers, invalid indexes, missing keys and accessor exceptions within normal try_get handling. Stored absence and failed access remain indistinguishable through that result; use the collection's explicit API when their distinction matters.
15.3 Dictionary coalescing assignment
CS-INDEX-004. For a map/dictionary-like target, map[key] ??= expression inserts when the key is absent, replaces when the existing value is nothing and leaves a present non-nothing value unchanged. The right side executes only for insertion or null fill.
CS-INDEX-005. Receiver/key expressions are not duplicated. Failures other than designated missing-key lookup are not silently swallowed. Ordinary non-map indexers retain the general coalescing-assignment contract.
CS-INDEX-006. Ordinary dictionary coalescing assignment is not an atomic concurrent get-or-add guarantee. Concurrent application code uses an explicitly appropriate collection/coordination API.
cache[key] ??= LoadCustomer(key)
Empty String, zero, false and empty collections are present values and remain stored. Map write capability and key/value compatibility are still required.
For a bound Dictionary <of String, Customer>, the conceptual C# lowering is:
var map = GetCache();
var key = GetKey();
if (!map.TryGetValue(key, out var current) || current is null)
map[key] = LoadCustomer(key);
This is an applicable dictionary lowering, not a universal rewrite of arbitrary custom indexers.
15.4 Enumeration and LINQ
CS-ENUMERATION-001. For_each follows the selected enumerable protocol without converting it to a list automatically. Ordinary asynchronous enumerable sources use asynchronous acquisition/disposal; a separate await for_each keyword is not needed. Loop behavior and metadata are defined in chapter 7 and concurrency in chapter 17.
CS-ENUMERATION-002. Ordinary LINQ calls retain .NET semantics. IEnumerable pipelines can execute lazily, observe later source changes, repeat work on repeated enumeration and throw during enumeration. IQueryable targets retain expression trees and provider translation. ToList/ToArray explicitly materialize; optimizations must preserve observation, side effects, ordering, enumeration count and exception timing.
let active = customers.Where(.IsActive)
let names = active.Select(.Name).ToList()
The first local holds a pipeline; the second holds a snapshot produced when ToList enumerates. Scope exit alone does not force the first pipeline to execute.
Inclusive positional selection
The helper examples assume DirectSharp.Collections is imported or the static SequenceExtensions member is explicitly selected. CS-SEQUENCE-013 defines export/binding and Integer inputs; ordinary imported LINQ retains its own contract.
CS-SEQUENCE-001. The DirectSharp standard-library two-position Take(start, end) requires the complete interval from start through end inclusively, with zero-based indexing. A negative start, negative end or end below start throws ArgumentOutOfRangeException at the call, before source enumeration, even when the source is empty. If the source ends before the complete interval can be supplied, throw ArgumentOutOfRangeException during enumeration; this includes a start beyond the source and an empty source. Do not clamp positions, clip a strict interval or fabricate values. Do not perform a separate Count pass. These rules do not change imported LINQ Take(count), CLR Range overloads or range-loop grammar.
CS-SEQUENCE-002. The two-position Take helper returns a deferred streaming IEnumerable<T>. Capture the receiver and arguments once at the call; validate numeric bounds without enumerating. Each enumeration starts fresh source traversal through an enumerator or an equivalent permitted indexed path and observes the source at that time; no list conversion, cached enumerator or implicit snapshot is supplied. A valid prefix can be yielded before a later source-length failure. A consumer stopping early need not discover a shortage in the unvisited suffix. Traverse in source order, stop once the inclusive interval is supplied and dispose the source enumerator on completion, exception or early consumer exit. Preserve ordinary source failures rather than converting them into bounds failures. Position arithmetic must not overflow solely because end is Integer.MaxValue. Invalid numeric bounds fail before acquiring a source enumerator; source-length validation stays deferred.
For a source containing integers zero through six:
| Call | Result |
|---|---|
items.Take(-3, 2) |
ArgumentOutOfRangeException at the call |
items.Take(2, 2) |
2 |
items.Take(10, 5) |
ArgumentOutOfRangeException at the call |
items.Take(5, 10) |
Yields 5, 6, then ArgumentOutOfRangeException when enumeration continues |
items.Take(20, 30) |
ArgumentOutOfRangeException during enumeration |
items.TryTake(-3, 2) |
0, 1, 2 |
items.TryTake(-3, -1) |
0: both negative positions normalize to zero |
items.TryTake(5, 10) |
5, 6 |
items.TryTake(10, 5) |
nothing without source traversal |
items.TryTake(20, 30) |
nothing; use available length or one traversal |
emptyItems.TryTake(0, 0) |
nothing |
CS-SEQUENCE-003. An empty sequence is a present value. Coalescing checks absence under CS-EXPR-008/009; it neither rejects a present empty sequence nor catches a later enumeration failure. In particular, Take(start, end) ?? throw does not replace strict Take's deferred source-length exception. Use require/ensure for additional application constraints. Emptiness testing may enumerate a deferred sequence; explicitly materialize once when a check and later processing must share one snapshot. An explicit conditional expression can turn a failed non-empty condition into nothing before ?? throw without changing coalescing semantics.
CS-SEQUENCE-004. The DirectSharp standard-library TryTake(start, end) is the tolerant counterpart. After CS-SEQUENCE-011 handles a missing source, normalize each negative position to zero, then test the adjusted interval. If end is still below start, return actual nothing at the call without acquiring or advancing the source. An end beyond the source selects through the last available item rather than throwing a bounds error. Positions remain zero-based and inclusive. Do not repair a reversed adjusted interval by swapping its bounds or manufacture source items.
CS-SEQUENCE-005. For a source without a suitable metadata/indexing fast path, TryTake traverses at the call in one forward pass, stopping after the inclusive requested end or source exhaustion. CS-SEQUENCE-006 permits equivalent collection optimizations. Retain only the selected items. Do not enumerate to compute a count, probe and restart, or read beyond the selected end. An available array Length or known collection Count may be used under CS-SEQUENCE-006. Return actual nothing when no selected item exists, including an empty source or start beyond the source. Otherwise return the collected selection through a present sequence result; the declared CLR result is nullable IReadOnlyList<T> under CS-SEQUENCE-012. Later enumeration reuses those collected values without rereading the source. This is a shallow selection: retaining an object reference does not deep-copy it or freeze its members. The exact exported container layout remains a library API concern.
CS-SEQUENCE-006. Available non-enumerating length/count metadata may optimize positional selection. Arrays and ordinary List collections can support direct indexed copying of the selected window and immediate detection of an unavailable start, without traversing skipped items. A source without a suitable fast path retains the bounded single forward pass. Do not use an enumerating Count operation as a prepass. A Count property alone does not establish that an arbitrary custom collection's indexer and enumerator are interchangeable: preserve selected values/order, normalization, absence, source failures and supported mutation behavior. Do not bypass a custom enumerator/getter's observable behavior merely because the object implements a collection interface.
For strict Take, observe mutable source metadata at the traversal boundary rather than caching a stale list count across the call and later enumeration. Preserve deferred execution and its established prefix/error timing; knowing the length does not authorize throwing at the call or rejecting an interval before delivering its otherwise-observable valid prefix. For TryTake, any indexed fast path must still return a collected selection independent of later source membership changes, or actual nothing when no items are selected. This optimization adds no thread-safety or concurrent-mutation guarantee.
TryTake therefore can block or fail while reading the source before returning. Acquisition, traversal and cleanup occur at the call, and the enumerator is disposed before return or on failure. There is no successful partial result if reading fails. Its bounded collection differs from strict Take's deferred streaming; neither requires a separate Count pass.
TryTake handles interval normalization and source exhaustion only. Computing the receiver/arguments retains normal evaluation and required-receiver rules. Source acquisition, traversal and cleanup failures propagate, including ArgumentOutOfRangeException raised by the source itself. Do not implement TryTake with a broad exception catch around source traversal. Coalescing checks the returned value's presence rather than its items.
let window = items.TryTake(start, finish) ?? throw
This is an action-body fragment with a present items sequence and Integer start/finish inputs. The short throw uses CS-EXPR-009: a missing method-call result selects NullReferenceException with the source-expression diagnostic. It does not recover Take's ArgumentOutOfRangeException or infer an exception from the helper's name. Use an explicit exception when the application wants that category. For this inclusive TryTake, no selected items produces actual absence, so the shorthand also handles a start beyond the source. It does not catch a source failure thrown while TryTake reads the source.
Count-taking selection and skipping
CS-SEQUENCE-007. DirectSharp's count-taking Take(count), Skip(count) and TakeLast(count) are strict library operations. A negative count throws ArgumentOutOfRangeException at the call, before source traversal. Take requires count leading items; TakeLast requires count trailing items; Skip requires count items to skip before yielding the remainder. If fewer than count items exist, throw ArgumentOutOfRangeException rather than clipping the strict request. Zero is valid: Take(0) and TakeLast(0) return present empty sequences without reading source elements, and Skip(0) yields the source items. Skipping exactly the source length is valid and yields present empty. Preserve source order. These count-taking rules do not change the two-position inclusive interval or imported CLR/LINQ contracts.
CS-SEQUENCE-008. Strict count-taking helpers retain deferred source traversal: validate negative count at the call, then observe source elements when enumerated. Each enumeration traverses afresh. Take stops after count items and can deliver a valid prefix before discovering a shortage; early consumer exit need not discover it. Skip detects shortage while skipping, before yielding any remainder, and does not read Current for skipped items. For a source without suitable metadata, TakeLast must reach exhaustion before yielding its selected tail; one forward pass with a bounded tail buffer suffices. Do not count by enumeration and restart, read beyond a bounded prefix unnecessarily or allocate a buffer sized to a huge requested count before input arrives. Preserve original source failures and dispose acquired source enumerators on completion, failure or early exit. CS-SEQUENCE-006 permits equivalent safe metadata/indexing optimizations without changing deferred observation/error timing.
For seven source items numbered zero through six:
| Call | Result |
|---|---|
items.Take(3) |
0, 1, 2 |
items.Take(10) |
Yields available prefix, then ArgumentOutOfRangeException if traversal continues |
items.Skip(5) |
5, 6 |
items.Skip(7) |
Present empty sequence |
items.Skip(10) |
ArgumentOutOfRangeException while skipping |
items.TakeLast(3) |
4, 5, 6 |
items.TakeLast(10) |
ArgumentOutOfRangeException after source exhaustion, before yielding a tail |
items.Take(0) / items.TakeLast(0) |
Present empty sequence |
CS-SEQUENCE-009. Count-taking TryTake(count) and TryTakeLast(count) normalize negative count to zero and return actual nothing when no items are selected, including normalized zero or an empty source. Otherwise return a collected selection in source order. TryTake collects at most count leading items at the call and clips a short source; do not advance beyond the requested prefix. TryTakeLast collects the available trailing items at the call, clipping a short source; a general enumerable requires traversal to exhaustion, with a bounded tail buffer sufficient. A zero selection does not require source traversal. Returned selections reuse collected values without rereading the source and do not deep-copy referenced objects. No source-reading/cleanup failure is converted into nothing or a successful partial selection. Dispose before return or on failure. Available metadata may optimize under CS-SEQUENCE-006.
CS-SEQUENCE-010. TrySkip(count) normalizes negative count to zero, skips available items and collects the remaining items at the call. A count beyond the source or no remaining items returns actual nothing. TrySkip(0) therefore returns a collected selection of all available items, or nothing for an empty source. A positive count whose skipping reaches source exhaustion returns nothing rather than throwing a bounds error. A nonempty result reuses the collected values without rereading source. For a general enumerable, obtaining the remainder requires reaching source exhaustion; an unbounded source cannot complete this call, and a large remainder requires storage. Do not count by enumeration first, read Current for skipped values or restart the source. Source acquisition/reading/cleanup failures propagate; dispose before return/failure. Equivalent safe metadata/indexing paths remain permitted.
| Call over items 0 through 6 | Result |
|---|---|
items.TryTake(10) / items.TryTakeLast(10) |
All seven items |
items.TryTake(0) / items.TryTakeLast(-2) |
nothing |
items.TrySkip(5) |
5, 6 |
items.TrySkip(7) / items.TrySkip(10) |
nothing |
items.TrySkip(-2) |
All seven items |
These are contracts of the DirectSharp library symbol actually bound. They do not override Enumerable.Take/Skip/TakeLast or introduce import-order priority. Apply normal name/overload resolution and ambiguity diagnostics. Empty strict results remain present; absence from a Try helper participates in the existing coalescing/contextual throw rules.
CS-SEQUENCE-011. DirectSharp's strict Take(start, end), Take(count), Skip(count) and TakeLast(count) reject a nothing source immediately with ArgumentNullException, before numeric-bound validation, even for a zero request. Their Try counterparts return actual nothing immediately for a nothing source, before numeric normalization or source work. A missing source is therefore tolerated by TryTake/TrySkip/TryTakeLast without acquiring an enumerator. For present sources, CS-SEQUENCE-001–010 retain all selected strict/tolerant bounds, zero, timing and source-error behavior. Source acquisition/reading/cleanup failures still propagate; they are not converted into absence. These rules do not alter imported LINQ or general member/null-safe access binding.
CS-SEQUENCE-012. Inclusive/count TryTake, TrySkip and TryTakeLast expose nullable IReadOnlyList<T> results. A present result provides Count and zero-based indexed access to the already-collected selection; reusing it does not reread the source. Missing selection/source retains CS-SEQUENCE-009–011's actual absence. This interface supplies no mutation members, but does not promise deep immutability, frozen referenced objects or protection from a cast to a concrete implementation. Strict deferred helper results remain separate; concrete backing/container placement does not change these semantics.
15.5 Destructive and forward-only consumption
CS-CONSUME-001. consume item from source ... end consume acquires sequentially by default until the source reports permanent completion. A source must implement IConsumable <of T> or have a recognized consumable adapter. Arrays, ordinary lists and arbitrary IEnumerable do not become consumable merely by being enumerable.
consume job from jobs
ProcessJob(job)
end consume
The jobs source must be an applicable consumable. Its protocol distinguishes completion from temporary unavailability, supports cancellation where acquisition can wait and async acquisition where waiting is naturally asynchronous. An empty-but-open producer channel is not complete.
CS-CONSUME-002. Queue adapters dequeue until empty; Stack adapters pop until empty; BlockingCollection and ChannelReader wait/acquire until completion. An open-ended ConcurrentQueue requires a completion-signaling wrapper rather than treating one empty probe as permanent completion. Custom consumables provide the same semantic distinctions.
CS-CONSUME-003. An IDataReader consumes Read until false in the current result set. DbDataReader may use ReadAsync with the applicable cancellation token. The row local is an IDataRecord view of the current reader position, not a detached snapshot. Advancing changes that view. NextResult/NextResultAsync is never called implicitly.
using reader = command.ExecuteReader()
consume row from reader
let id = row.GetGuid(0)
let name = row.GetString(1)
ProcessCustomer(id, name)
end consume
end using
The command is an ordinary database command with the indicated result schema. Values retained beyond an iteration must be projected/copied into an independent model. Disposal is provided by the surrounding using, not implied by consumption.
CS-CONSUME-004. concurrent consume ... with limit: value overlaps bodies only when acquisition is safely supported. The limit obeys the positive-integer/Gate contract. Completion joins active bodies after permanent source completion. Direct concurrent consumption of a raw IDataReader/DbDataReader is a compilation error because its cursor is shared mutable state. A safe independent projection/adapter is required. Where results are collected, consumption order is preserved under the collection result contract.
Consume has no query where clauses. Filtering and transformation use ordinary body statements or an adapter. No slicing/range-index syntax or tuple-destructuring declaration is introduced by these collection facilities; extraction/shaping uses ordinary APIs such as Take/Skip/TakeLast.
Consumable CLR contract and adapters
CS-CONSUME-005. The DirectSharp library exposes IConsumable<T> with the ordinary CLR member ValueTask<ConsumeResult<T>> AcquireAsync(CancellationToken cancellation = default). ConsumeResult<T> is a readonly value type with Boolean HasItem and a typed Item getter. FromItem(value) represents an acquired item, including null/default values allowed by T. Completed and the default result represent permanent completion with HasItem false; reading Item then throws InvalidOperationException. Do not treat a null, zero, false or empty payload as completion. A temporarily empty live source waits rather than returning Completed. Await each ValueTask operation according to its normal .NET contract; the interface does not change DirectSharp async action ABI rules.
CS-CONSUME-006. Consumable adapters borrow their .NET sources. Completion, early loop exit, cancellation or failure does not transfer disposal ownership, close the underlying source or automatically dispose the delivered application item. Source ownership stays with its caller/resource scope. Forward cancellation to native cancellable waits/reads where supported; synchronous sources without native cancellation are not made forcibly interruptible. Propagate source faults/cancellation rather than translating them into successful completion. No automatic producer cancellation, acknowledgement, retry or requeue protocol is supplied. Once an adapter session reports permanent completion, it does not resume that session; a later explicit reader result or a new queue drain can use a new session.
CS-CONSUME-007. Standard adapter coverage includes Queue/Stack drain sessions, BlockingCollection, ChannelReader, current-result-only IDataReader/DbDataReader, and completion-aware wrappers for ConcurrentQueue/ConcurrentStack/ConcurrentBag. Queue/Stack end their drain session at empty. Live concurrent collections require coordinated arrival notification and explicit producer completion followed by drain; a thread-safe TryDequeue/TryPop/TryTake or temporary emptiness alone is insufficient. Preserve native FIFO/LIFO order where defined; ConcurrentBag has no invented ordering guarantee. BlockingCollection is natively blocking; an async bridge may use a worker for that wait and must settle it, not claim native nonblocking I/O. Channels already provide async waiting/completion and retain native failures after buffered items. Capability/registration details do not make every IConsumable implementation safe for concurrent acquisition.
CS-CONSUME-008. Text and binary units are selected through explicit ordinary library adapters. A TextReader/StreamReader line adapter yields String lines, including a blank line as an item, and maps native null/EOF to completion; encoding/newline removal follows the reader's own .NET contract. A Stream byte-chunk adapter requires a positive chunk size (otherwise ArgumentOutOfRangeException), returns independent Byte[] chunks, preserves short positive reads as items, and maps a zero-byte read with a positive requested size to EOF/completion. It requires no seek/Length support and does not reuse a live buffer for previously delivered chunks. Sources stay borrowed. Shared text/binary cursors require appropriate acquisition serialization before overlapping use; stable item buffers alone do not prove concurrent source safety. Raw streams do not gain an implicit line-versus-byte interpretation, and arrays/lists/arbitrary enumerables remain non-consumable without an explicit adapter.
The acquisition/result contract is supplemented by CS-CONSUME-011–013 factory/producer members; private compiler registration metadata remains separate. Raw database readers retain CS-CONSUME-003/004: IDataRecord values are ephemeral, the current result set is the boundary and direct concurrent raw-reader consume is invalid. An explicit NextResult followed by another consume creates a new result-set session; it is not a hidden side effect of completion.
CS-CONSUME-009. The ordinary CLR marker interface DirectSharp.IConcurrentConsumable<T> derives from IConsumable<T> and adds no acquisition member. It declares that overlapping acquisition is supported and that acquired items remain valid across subsequent acquisitions while concurrent bodies use them. It is a library capability contract, not a keyword, proof of arbitrary implementation correctness or a guarantee that mutable payload objects are thread-safe. Compiler-known native adapters retain their existing capability handling. An arbitrary IConsumable implementation alone supplies no such promise. Direct concurrent raw IDataReader/DbDataReader consumption remains forbidden; serializing cursor advancement while returning an ephemeral row view does not satisfy the capability.
CS-CONSUME-010. When a completion-aware live collection wrapper explicitly receives a producer failure, close producer admission, preserve buffered items in their native order and deliver them before surfacing that original failure once drained. A failed terminal state must not return successful Completed; later acquisitions retain the original failure. Pending waiters must be able to observe arrivals or this failed terminal outcome rather than wait forever on a closed producer. ConcurrentBag retains its unspecified order. This policy does not reinterpret caller cancellation, body failure or arbitrary native collection operations. Native ChannelReader adapters keep their own native buffered/completion/failure behavior. No automatic acknowledgement, retry, requeue or ownership transfer is introduced, and exact factory/failure-reporting member names remain separate declarations.
15.6 Reference lowering and diagnostics
For a raw synchronous data reader, the core loop is:
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
IDataRecord row = reader;
Guid id = row.GetGuid(0);
string name = row.GetString(1);
ProcessCustomer(id, name);
}
}
A C# reference loop over the CLR consumable contract can borrow its source and process each item without using null as an end marker:
static async Task DrainAsync<T>(DirectSharp.IConsumable<T> source,
Func<T, Task> processAsync, CancellationToken cancellation)
{
while (true)
{
DirectSharp.ConsumeResult<T> next = await source.AcquireAsync(cancellation);
if (!next.HasItem) break;
await processAsync(next.Item);
}
}
The caller owns source cleanup. Adapter factories/capability registration are implementation concerns; this loop does not infer safe concurrent acquisition or make reader rows independent snapshots.
A reference C# pattern for two-position library selection validates source presence before numeric bounds:
static IEnumerable<T> TakeInclusive<T>(IEnumerable<T> source, int start, int end)
{
ArgumentNullException.ThrowIfNull(source);
if (start < 0) throw new ArgumentOutOfRangeException(nameof(start));
if (end < 0 || end < start) throw new ArgumentOutOfRangeException(nameof(end));
return Enumerate();
IEnumerable<T> Enumerate()
{
using var iterator = source.GetEnumerator();
for (long position = 0; position <= end; position++)
{
if (!iterator.MoveNext())
throw new ArgumentOutOfRangeException(nameof(end),
"The complete interval is unavailable.");
if (position >= start) yield return iterator.Current;
}
}
}
The tolerant counterpart can use known array/List metadata and copy only the selected window, falling back to one forward pass:
static IReadOnlyList<T>? TryTakeInclusive<T>(IEnumerable<T>? source, int start, int end)
{
if (source is null) return null;
start = Math.Max(0, start);
end = Math.Max(0, end);
if (end < start) return null;
// Known built-in shapes permit metadata/indexed copying. Do not treat
// arbitrary custom Count/indexer/enumerator implementations as equivalent.
if (source is T[] array)
{
int last = Math.Min(end, array.Length - 1);
if (start > last) return null;
var copied = new T[last - start + 1];
Array.Copy(array, start, copied, 0, copied.Length);
return copied;
}
if (source is List<T> list && list.GetType() == typeof(List<T>))
{
int last = Math.Min(end, list.Count - 1);
if (start > last) return null;
return list.GetRange(start, last - start + 1);
}
using var iterator = source.GetEnumerator();
List<T>? selected = null;
for (long position = 0; position <= end && iterator.MoveNext(); position++)
{
if (position < start) continue;
(selected ??= new List<T>()).Add(iterator.Current);
}
return selected; // No selected item means actual absence, with no Count pass.
}
The exact built-in array/List cases use their available Length/Count and copy only the selected items. Other sources allocate a buffer when an item is first selected; source exhaustion before that point returns null naturally, without an enumerating count or restart. Stop without an extra MoveNext beyond end, and dispose before returning. The backing List illustrates collection; it is not a finalized exported result-container signature.
Numeric bounds validation is outside the iterator so invalid arguments throw at the call; source traversal and source-length failure stay deferred. A prefix may already have been yielded when a later MoveNext detects a shortage. Early exit does not validate the unvisited suffix. The helper name is illustrative rather than an exported extension-container declaration. A wider position avoids overflow at the maximum inclusive Integer end. The deferred iterator owns its source enumerator and stops without requesting an item beyond the selected interval. The pattern assumes a present source; it does not define other helpers' null-receiver contracts or provider-specific IQueryable translation.
Strict and tolerant count-based skipping can share the same forward traversal idea while preserving different outcomes and execution timing. These illustrative helpers validate strict source presence or return absence for a missing Try source; safe collection fast paths are optional:
static IEnumerable<T> StrictSkip<T>(IEnumerable<T> source, int count)
{
ArgumentNullException.ThrowIfNull(source);
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count));
return Enumerate();
IEnumerable<T> Enumerate()
{
using var iterator = source.GetEnumerator();
for (int skipped = 0; skipped < count; skipped++)
if (!iterator.MoveNext()) throw new ArgumentOutOfRangeException(nameof(count));
while (iterator.MoveNext()) yield return iterator.Current;
}
}
static IReadOnlyList<T>? TrySkip<T>(IEnumerable<T>? source, int count)
{
if (source is null) return null;
count = Math.Max(0, count);
using var iterator = source.GetEnumerator();
for (int skipped = 0; skipped < count; skipped++)
if (!iterator.MoveNext()) return null;
List<T>? selected = null;
while (iterator.MoveNext()) (selected ??= new List<T>()).Add(iterator.Current);
return selected;
}
StrictSkip's outer validation is immediate and its iterator defers source work. TrySkip completes source traversal/cleanup before returning an optional collected result. TakeLast can instead keep a queue of at most count items: discard the oldest item when full, append the next, and reach source exhaustion once. The strict form then rejects a shorter tail; the Try form returns the available nonempty tail or null. A zero count avoids reading a tail entirely. These private reference shapes illustrate execution; CS-SEQUENCE-013 specifies the exported strict/Try family.
The reader is never advanced concurrently or copied implicitly. A nullable numeric acquisition result needs an actual absence-capable CLR representation rather than a sentinel. Required diagnostics include mixed literal forms, invalid target conversions, constant duplicate/null keys, incompatible indexers, non-writable coalescing targets, unsupported consumable sources and direct concurrent reader consumption.
15.7 Exported factories and sequence helpers
CS-CONSUME-011. DirectSharp.Consumables is the static factory class with FromQueue(Queue<T>), FromStack(Stack<T>), FromBlockingCollection(BlockingCollection<T>), FromChannel(ChannelReader<T>), FromReader(IDataReader), FromReader(DbDataReader), ReadLines(TextReader) and ReadChunks(Stream, Integer), with generic forms and result types in the API catalogue. Reject a null source with ArgumentNullException before source work; ReadChunks then rejects nonpositive chunkSize with ArgumentOutOfRangeException. Creation does not read, enumerate or take disposal ownership. Each adapter is one session; reader sessions stay in the current result set. BlockingCollection/channel factories return IConcurrentConsumable; other listed factories return IConsumable and advertise no new concurrent capability. Native access may be serialized within one adapter to honor its source contract; no global coordination across adapters or unrelated native reads is implied. Known native consume sources require no factory syntax. Existing payload, cancellation, completion, cursor and raw-reader restrictions remain.
CS-CONSUME-012. DirectSharp.ILiveConsumable<T> extends IConcurrentConsumable<T> with void Add(T item), bool TryComplete() and bool TryComplete(Exception failure). Consumables.FromConcurrentQueue, FromConcurrentStack and FromConcurrentBag accept their corresponding native generic collection and return ILiveConsumable<T>; a null source throws ArgumentNullException without acquisition. One wrapper coordinates an active producer session, borrowing its collection and items. Producers use wrapper Add for admission/insertion/notification; uncoordinated native writes cannot promise notification or race its completion protocol. Workers may receive the narrower IConcurrentConsumable contract. No acknowledgement, retry/requeue, bounded capacity, payload thread safety or disposal ownership is added.
CS-CONSUME-013. A live wrapper's first terminal transition wins atomically. TryComplete returns true only for that transition; subsequent terminal calls return false without replacing its completion/failure. The failure overload rejects null with ArgumentNullException before attempting transition; the no-argument form records success. Add ordered before closure contributes an item; Add after closure throws InvalidOperationException and admits nothing. Drain buffered items in native order before successful Completed or the retained original producer failure under CS-CONSUME-010. A cancelled consumer wait or failed consumer body is not a producer terminal transition. No automatic cancellation check on already-available items is added.
CS-SEQUENCE-013. DirectSharp.Collections.SequenceExtensions is the opt-in static extension container for the eight selected strict/Try Take(count), Take(start, end), Skip(count), TakeLast(count) and counterpart declarations in the API catalogue. Counts/positions are Integer/System.Int32; no Long overload is selected. Strict results remain IEnumerable<T> and Try results nullable IReadOnlyList<T> under CS-SEQUENCE-001–012. The automatic DirectSharp root import does not import this child namespace. Callers import it or qualify the static container. Imported LINQ retains its contracts; equally applicable extensions use ordinary ambiguity diagnostics without helper priority. No concrete collected container or First/Last export is selected by this rule.
Specification contents · Previous: 14. Receiver and resource scopes · Next: 16. Predicates and pattern matching