gio

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 18, 2023 License: MIT Imports: 3 Imported by: 7

README

gio

A generic I/O library for Go


Overview

Remember the library we all know and love? "io"? This library extends the usefulness of the interfaces and functions exposed by the standard library (for byte slices) with generics. The core functionality of a reader (something that reads) should be common amongst any type, provided that the implementation can handle the calls (to read, to write, or whatever action).

This way, despite not having the same fluidity as the standard library's implementation in some levels (there is no WriteString function or StringWriter interface), it promises to allow the same API to be transposed to other defined types.

Other than this, all functionality from the standard library is present in this generic I/O library.

Why generics?

Generics are great for when there is a solid algorithm that serves for many types, and can be abstracted enough to work without major workarounds; and this approach to a I/O library is very idiomatic and so simple (the Go way). Of course, the standard library's implementation has some other packages in mind that work together with io, such as strings and bytes. The approach with generics will limit the potential that shines in the original implementation, one way or the other (simply with the fact that if you need to write something to a writer, you need to implement it; there is no basic WriteString function for this).

But all in all, it was a great exercise to practice using generics. Maybe I will just use this library once or twice, maybe it will be actually useful for some. I am just in it for the ride. :)

Disclaimer

This library will mirror all logic from Go's (standard) io library; and change the []byte implementation with a generic T any and []T implementation. There are no changes in the actual logic in the library.


Documentation

Index

Examples

Constants

View Source
const (
	SeekStart   = 0 // seek relative to the origin of the source
	SeekCurrent = 1 // seek relative to the current offset
	SeekEnd     = 2 // seek relative to the end
)

Seek whence values.

Variables

View Source
var ErrClosedPipe = io.ErrClosedPipe

ErrClosedPipe is the error used for read or write operations on a closed pipe.

View Source
var ErrInvalidWrite = errors.New("invalid write result")

ErrInvalidWrite means that a write returned an impossible count.

View Source
var ErrNoProgress = io.ErrNoProgress

ErrNoProgress is returned by some clients of a Reader when many calls to Read have failed to return any data or error, usually the sign of a broken Reader implementation.

View Source
var ErrOffset = errors.New("Seek: invalid offset")
View Source
var ErrShortBuffer = io.ErrShortBuffer

ErrShortBuffer means that a read required a longer buffer than was provided.

View Source
var ErrShortWrite = io.ErrShortWrite

ErrShortWrite means that a write accepted fewer T items than requested but failed to return an explicit error.

View Source
var ErrUnexpectedEOF = io.ErrUnexpectedEOF

ErrUnexpectedEOF means that EOF was encountered in the middle of reading a fixed-size block or data structure.

View Source
var ErrWhence = errors.New("Seek: invalid whence")

Functions

func Copy

func Copy[T any](dst Writer[T], src Reader[T]) (written int64, err error)

Copy copies from src to dst until either EOF is reached on src or an error occurs. It returns the number of T items copied and the first error encountered while copying, if any.

A successful Copy returns err == nil, not err == EOF. Because Copy is defined to read from src until EOF, it does not treat an EOF from Read as an error to be reported.

If src implements the WriterTo interface, the copy is implemented by calling src.WriteTo(dst). Otherwise, if dst implements the ReaderFrom interface, the copy is implemented by calling dst.ReadFrom(src).

Example
package main

import (
	"log"
	"os"
	"strings"

	"github.com/zalgonoise/gio"
)

func main() {
	r := strings.NewReader("some io.Reader stream to be read\n")

	if _, err := gio.Copy[byte](os.Stdout, r); err != nil {
		log.Fatal(err)
	}

}
Output:

some io.Reader stream to be read

func CopyBuffer

func CopyBuffer[T any](dst Writer[T], src Reader[T], buf []T) (written int64, err error)

CopyBuffer is identical to Copy except that it stages through the provided buffer (if one is required) rather than allocating a temporary one. If buf is nil, one is allocated; otherwise if it has zero length, CopyBuffer panics.

If either src implements WriterTo or dst implements ReaderFrom, buf will not be used to perform the copy.

Example
package main

import (
	"log"
	"os"
	"strings"

	"github.com/zalgonoise/gio"
)

func main() {
	r1 := strings.NewReader("first reader\n")
	r2 := strings.NewReader("second reader\n")
	buf := make([]byte, 8)

	// buf is used here...
	if _, err := gio.CopyBuffer[byte](os.Stdout, r1, buf); err != nil {
		log.Fatal(err)
	}

	// ... reused here also. No need to allocate an extra buffer.
	if _, err := gio.CopyBuffer[byte](os.Stdout, r2, buf); err != nil {
		log.Fatal(err)
	}

}
Output:

first reader
second reader

func CopyN

func CopyN[T any](dst Writer[T], src Reader[T], n int64) (written int64, err error)

CopyN copies n T items (or until an error) from src to dst. It returns the number of T items copied and the earliest error encountered while copying. On return, written == n if and only if err == nil.

If dst implements the ReaderFrom interface, the copy is implemented using it.

Example
package main

import (
	"log"
	"os"
	"strings"

	"github.com/zalgonoise/gio"
)

func main() {
	r := strings.NewReader("some io.Reader stream to be read")

	if _, err := gio.CopyN[byte](os.Stdout, r, 4); err != nil {
		log.Fatal(err)
	}

}
Output:

some

func Pipe

func Pipe[T any]() (*PipeReader[T], *PipeWriter[T])

Pipe creates a synchronous in-memory pipe. It can be used to connect code expecting an io.Reader with code expecting an io.Writer.

Reads and Writes on the pipe are matched one to one except when multiple Reads are needed to consume a single Write. That is, each Write to the PipeWriter blocks until it has satisfied one or more Reads from the PipeReader that fully consume the written data. The data is copied directly from the Write to the corresponding Read (or Reads); there is no internal buffering.

It is safe to call Read and Write in parallel with each other or with Close. Parallel calls to Read and parallel calls to Write are also safe: the individual calls will be gated sequentially.

Example
package main

import (
	"fmt"
	"log"
	"os"

	"github.com/zalgonoise/gio"
)

func main() {
	r, w := gio.Pipe[byte]()

	go func() {
		fmt.Fprint(w, "some io.Reader stream to be read\n")
		w.Close()
	}()

	if _, err := gio.Copy[byte](os.Stdout, r); err != nil {
		log.Fatal(err)
	}

}
Output:

some io.Reader stream to be read

func ReadAll

func ReadAll[T any](r Reader[T]) ([]T, error)

ReadAll reads from r until an error or EOF and returns the data it read. A successful call returns err == nil, not err == EOF. Because ReadAll is defined to read from src until EOF, it does not treat an EOF from Read as an error to be reported.

Example
package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/zalgonoise/gio"
)

func main() {
	r := strings.NewReader("Go is a general-purpose language designed with systems programming in mind.")

	b, err := gio.ReadAll[byte](r)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%s", b)

}
Output:

Go is a general-purpose language designed with systems programming in mind.

func ReadAtLeast

func ReadAtLeast[T any](r Reader[T], buf []T, min int) (n int, err error)

ReadAtLeast reads from r into buf until it has read at least min T items. It returns the number of T items copied and an error if fewer T items were read. The error is EOF only if no T items were read. If an EOF happens after reading fewer than min T items, ReadAtLeast returns ErrUnexpectedEOF. If min is greater than the length of buf, ReadAtLeast returns ErrShortBuffer. On return, n >= min if and only if err == nil. If r returns an error having read at least min T items, the error is dropped.

Example
package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/zalgonoise/gio"
)

func main() {
	r := strings.NewReader("some io.Reader stream to be read\n")

	buf := make([]byte, 14)
	if _, err := gio.ReadAtLeast[byte](r, buf, 4); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s\n", buf)

	// buffer smaller than minimal read size.
	shortBuf := make([]byte, 3)
	if _, err := gio.ReadAtLeast[byte](r, shortBuf, 4); err != nil {
		fmt.Println("error:", err)
	}

	// minimal read size bigger than io.Reader stream
	longBuf := make([]byte, 64)
	if _, err := gio.ReadAtLeast[byte](r, longBuf, 64); err != nil {
		fmt.Println("error:", err)
	}

}
Output:

some io.Reader
error: short buffer
error: unexpected EOF

func ReadFull

func ReadFull[T any](r Reader[T], buf []T) (n int, err error)

ReadFull reads exactly len(buf) T items from r into buf. It returns the number of T items copied and an error if fewer T items were read. The error is EOF only if no T items were read. If an EOF happens after reading some but not all the T items, ReadFull returns ErrUnexpectedEOF. On return, n == len(buf) if and only if err == nil. If r returns an error having read at least len(buf) T items, the error is dropped.

Example
package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/zalgonoise/gio"
)

func main() {
	r := strings.NewReader("some io.Reader stream to be read\n")

	buf := make([]byte, 4)
	if _, err := gio.ReadFull[byte](r, buf); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s\n", buf)

	// minimal read size bigger than io.Reader stream
	longBuf := make([]byte, 64)
	if _, err := gio.ReadFull[byte](r, longBuf); err != nil {
		fmt.Println("error:", err)
	}

}
Output:

some
error: unexpected EOF

Types

type Closer

type Closer io.Closer

Closer is the interface that wraps the basic Close method.

The behavior of Close after the first call is undefined. Specific implementations may document their own behavior.

type ItemReader

type ItemReader[T any] interface {
	ReadItem() (T, error)
}

ItemReader is the interface that wraps the ReadItem method.

ReadItem reads and returns the next item from the input or any error encountered. If ReadItem returns an error, no input item was consumed, and the returned item value is undefined.

ReadItem provides an efficient interface for item-at-time processing. A Reader that does not implement ItemReader can be wrapped using bufio.NewReader to add this method.

type ItemScanner

type ItemScanner[T any] interface {
	ItemReader[T]
	UnreadItem() error
}

ItemScanner is the interface that adds the UnreadItem method to the basic ReadItem method.

UnreadItem causes the next call to ReadItem to return the last Item read. If the last operation was not a successful call to ReadItem, UnreadItem may return an error, unread the last item read (or the item prior to the last-unread item), or (in implementations that support the Seeker interface) seek to one item before the current offset.

type ItemWriter

type ItemWriter[T any] interface {
	WriteItem(item T) error
}

ItemWriter is the interface that wraps the WriteItem method.

type LimitedReader

type LimitedReader[T any] struct {
	R Reader[T] // underlying reader
	N int64     // max T items remaining
}

A LimitedReader reads from R but limits the amount of data returned to just N T items. Each call to Read updates N to reflect the new amount remaining. Read returns EOF when N <= 0 or when the underlying R returns EOF.

func (*LimitedReader[T]) Read

func (l *LimitedReader[T]) Read(p []T) (n int, err error)

type PipeReader

type PipeReader[T any] struct {
	// contains filtered or unexported fields
}

A PipeReader is the read half of a pipe.

func (*PipeReader[T]) Close

func (r *PipeReader[T]) Close() error

Close closes the reader; subsequent writes to the write half of the pipe will return the error ErrClosedPipe.

func (*PipeReader[T]) CloseWithError

func (r *PipeReader[T]) CloseWithError(err error) error

CloseWithError closes the reader; subsequent writes to the write half of the pipe will return the error err.

CloseWithError never overwrites the previous error if it exists and always returns nil.

func (*PipeReader[T]) Read

func (r *PipeReader[T]) Read(data []T) (n int, err error)

Read implements the standard Read interface: it reads data from the pipe, blocking until a writer arrives or the write end is closed. If the write end is closed with an error, that error is returned as err; otherwise err is EOF.

type PipeWriter

type PipeWriter[T any] struct {
	// contains filtered or unexported fields
}

A PipeWriter is the write half of a pipe.

func (*PipeWriter[T]) Close

func (w *PipeWriter[T]) Close() error

Close closes the writer; subsequent reads from the read half of the pipe will return no T items and EOF.

func (*PipeWriter[T]) CloseWithError

func (w *PipeWriter[T]) CloseWithError(err error) error

CloseWithError closes the writer; subsequent reads from the read half of the pipe will return no T items and the error err, or EOF if err is nil.

CloseWithError never overwrites the previous error if it exists and always returns nil.

func (*PipeWriter[T]) Write

func (w *PipeWriter[T]) Write(data []T) (n int, err error)

Write implements the standard Write interface: it writes data to the pipe, blocking until one or more readers have consumed all the data or the read end is closed. If the read end is closed with an error, that err is returned as err; otherwise err is ErrClosedPipe.

type ReadCloser

type ReadCloser[T any] interface {
	Reader[T]
	Closer
}

ReadCloser is the interface that groups the basic Read and Close methods.

func NopCloser

func NopCloser[T any](r Reader[T]) ReadCloser[T]

NopCloser returns a ReadCloser with a no-op Close method wrapping the provided Reader r. If r implements WriterTo, the returned ReadCloser will implement WriterTo by forwarding calls to r.

type ReadSeekCloser

type ReadSeekCloser[T any] interface {
	Reader[T]
	Seeker
	Closer
}

ReadSeekCloser is the interface that groups the basic Read, Seek and Close methods.

type ReadSeeker

type ReadSeeker[T any] interface {
	Reader[T]
	Seeker
}

ReadSeeker is the interface that groups the basic Read and Seek methods.

type ReadWriteCloser

type ReadWriteCloser[T any] interface {
	Reader[T]
	Writer[T]
	Closer
}

ReadWriteCloser is the interface that groups the basic Read, Write and Close methods.

type ReadWriteSeeker

type ReadWriteSeeker[T any] interface {
	Reader[T]
	Writer[T]
	Seeker
}

ReadWriteSeeker is the interface that groups the basic Read, Write and Seek methods.

type ReadWriter

type ReadWriter[T any] interface {
	Reader[T]
	Writer[T]
}

ReadWriter is the interface that groups the basic Read and Write methods.

type Reader

type Reader[T any] interface {
	Read(p []T) (n int, err error)
}

Reader is the interface that wraps the basic Read method.

Read reads up to len(p) T into p. It returns the number of T items read (0 <= n <= len(p)) and any error encountered. Even if Read returns n < len(p), it may use all of p as scratch space during the call. If some data is available but not len(p) T items, Read conventionally returns what is available instead of waiting for more.

When Read encounters an error or end-of-file condition after successfully reading n > 0 T items, it returns the number of T items read. It may return the (non-nil) error from the same call or return the error (and n == 0) from a subsequent call. An instance of this general case is that a Reader returning a non-zero number of T items at the end of the input stream may return either err == EOF or err == nil. The next Read should return 0, EOF.

Callers should always process the n > 0 T items returned before considering the error err. Doing so correctly handles I/O errors that happen after reading some T items and also both of the allowed EOF behaviors.

Implementations of Read are discouraged from returning a zero T item count with a nil error, except when len(p) == 0. Callers should treat a return of 0 and nil as indicating that nothing happened; in particular it does not indicate EOF.

Implementations must not retain p.

func LimitReader

func LimitReader[T any](r Reader[T], n int64) Reader[T]

LimitReader returns a Reader that reads from r but stops with EOF after n T items. The underlying implementation is a *LimitedReader.

Example
package main

import (
	"log"
	"os"
	"strings"

	"github.com/zalgonoise/gio"
)

func main() {
	r := strings.NewReader("some io.Reader stream to be read\n")
	lr := gio.LimitReader[byte](r, 4)

	if _, err := gio.Copy[byte](os.Stdout, lr); err != nil {
		log.Fatal(err)
	}

}
Output:

some

func MultiReader

func MultiReader[T any](readers ...Reader[T]) Reader[T]

MultiReader returns a Reader that's the logical concatenation of the provided input readers. They're read sequentially. Once all inputs have returned EOF, Read will return EOF. If any of the readers return a non-nil, non-EOF error, Read will return that error.

Example
package main

import (
	"log"
	"os"
	"strings"

	"github.com/zalgonoise/gio"
)

func main() {
	r1 := strings.NewReader("first reader ")
	r2 := strings.NewReader("second reader ")
	r3 := strings.NewReader("third reader\n")
	r := gio.MultiReader[byte](r1, r2, r3)

	if _, err := gio.Copy[byte](os.Stdout, r); err != nil {
		log.Fatal(err)
	}

}
Output:

first reader second reader third reader

func TeeReader

func TeeReader[T any](r Reader[T], w Writer[T]) Reader[T]

TeeReader returns a Reader that writes to w what it reads from r. All reads from r performed through it are matched with corresponding writes to w. There is no internal buffering - the write must complete before the read completes. Any error encountered while writing is reported as a read error.

Example
package main

import (
	"io"
	"log"
	"os"
	"strings"

	"github.com/zalgonoise/gio"
)

func main() {
	var r io.Reader = strings.NewReader("some io.Reader stream to be read\n")

	r = gio.TeeReader[byte](r, os.Stdout)

	// Everything read from r will be copied to stdout.
	if _, err := gio.ReadAll[byte](r); err != nil {
		log.Fatal(err)
	}

}
Output:

some io.Reader stream to be read

type ReaderAt

type ReaderAt[T any] interface {
	ReadAt(p []T, off int64) (n int, err error)
}

ReaderAt is the interface that wraps the basic ReadAt method.

ReadAt reads len(p) T items into p starting at offset off in the underlying input source. It returns the number of T items read (0 <= n <= len(p)) and any error encountered.

When ReadAt returns n < len(p), it returns a non-nil error explaining why more T items were not returned. In this respect, ReadAt is stricter than Read.

Even if ReadAt returns n < len(p), it may use all of p as scratch space during the call. If some data is available but not len(p) T items, ReadAt blocks until either all the data is available or an error occurs. In this respect ReadAt is different from Read.

If the n = len(p) T items returned by ReadAt are at the end of the input source, ReadAt may return either err == EOF or err == nil.

If ReadAt is reading from an input source with a seek offset, ReadAt should not affect nor be affected by the underlying seek offset.

Clients of ReadAt can execute parallel ReadAt calls on the same input source.

Implementations must not retain p.

type ReaderFrom

type ReaderFrom[T any] interface {
	ReadFrom(r Reader[T]) (n int64, err error)
}

ReaderFrom is the interface that wraps the ReadFrom method.

ReadFrom reads data from r until EOF or error. The return value n is the number of T items read. Any error except EOF encountered during the read is also returned.

The Copy function uses ReaderFrom if available.

type SectionReader

type SectionReader[T any] struct {
	// contains filtered or unexported fields
}

SectionReader implements Read, Seek, and ReadAt on a section of an underlying ReaderAt.

Example
package main

import (
	"log"
	"os"
	"strings"

	"github.com/zalgonoise/gio"
)

func main() {
	r := strings.NewReader("some io.Reader stream to be read\n")
	s := gio.NewSectionReader[byte](r, 5, 17)

	if _, err := gio.Copy[byte](os.Stdout, s); err != nil {
		log.Fatal(err)
	}

}
Output:

io.Reader stream

func NewSectionReader

func NewSectionReader[T any](r ReaderAt[T], off int64, n int64) *SectionReader[T]

NewSectionReader returns a SectionReader that reads from r starting at offset off and stops with EOF after n T items.

func (*SectionReader[T]) Read

func (s *SectionReader[T]) Read(p []T) (n int, err error)
Example
package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/zalgonoise/gio"
)

func main() {
	r := strings.NewReader("some io.Reader stream to be read\n")
	s := gio.NewSectionReader[byte](r, 5, 17)

	buf := make([]byte, 9)
	if _, err := s.Read(buf); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%s\n", buf)

}
Output:

io.Reader

func (*SectionReader[T]) ReadAt

func (s *SectionReader[T]) ReadAt(p []T, off int64) (n int, err error)
Example
package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/zalgonoise/gio"
)

func main() {
	r := strings.NewReader("some io.Reader stream to be read\n")
	s := gio.NewSectionReader[byte](r, 5, 17)

	buf := make([]byte, 6)
	if _, err := s.ReadAt(buf, 10); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%s\n", buf)

}
Output:

stream

func (*SectionReader[T]) Seek

func (s *SectionReader[T]) Seek(offset int64, whence int) (int64, error)
Example
package main

import (
	"io"
	"log"
	"os"
	"strings"

	"github.com/zalgonoise/gio"
)

func main() {
	r := strings.NewReader("some io.Reader stream to be read\n")
	s := gio.NewSectionReader[byte](r, 5, 17)

	if _, err := s.Seek(10, io.SeekStart); err != nil {
		log.Fatal(err)
	}

	if _, err := gio.Copy[byte](os.Stdout, s); err != nil {
		log.Fatal(err)
	}

}
Output:

stream

func (*SectionReader[T]) Size

func (s *SectionReader[T]) Size() int64

Size returns the size of the section in T items.

Example
package main

import (
	"fmt"
	"strings"

	"github.com/zalgonoise/gio"
)

func main() {
	r := strings.NewReader("some io.Reader stream to be read\n")
	s := gio.NewSectionReader[byte](r, 5, 17)

	fmt.Println(s.Size())

}
Output:

17

type Seeker

type Seeker io.Seeker

Seeker is the interface that wraps the basic Seek method.

Seek sets the offset for the next Read or Write to offset, interpreted according to whence: SeekStart means relative to the start of the source, SeekCurrent means relative to the current offset, and SeekEnd means relative to the end (for example, offset = -2 specifies the penultimate T item of the source). Seek returns the new offset relative to the start of the source or an error, if any.

Seeking to an offset before the start of the source is an error. Seeking to any positive offset may be allowed, but if the new offset exceeds the size of the underlying object the behavior of subsequent I/O operations is implementation-dependent.

type StringWriter

type StringWriter io.StringWriter

StringWriter is the interface that wraps the WriteString method.

type WriteCloser

type WriteCloser[T any] interface {
	Writer[T]
	Closer
}

WriteCloser is the interface that groups the basic Write and Close methods.

type WriteSeeker

type WriteSeeker[T any] interface {
	Writer[T]
	Seeker
}

WriteSeeker is the interface that groups the basic Write and Seek methods.

type Writer

type Writer[T any] interface {
	Write(p []T) (n int, err error)
}

Writer is the interface that wraps the basic Write method.

Write writes len(p) T items from p to the underlying data stream. It returns the number of T items written from p (0 <= n <= len(p)) and any error encountered that caused the write to stop early. Write must return a non-nil error if it returns n < len(p). Write must not modify the slice data, even temporarily.

Implementations must not retain p.

var Discard Writer[any] = discard[any]{}

Discard is a Writer on which all Write calls succeed without doing anything.

func MultiWriter

func MultiWriter[T any](writers ...Writer[T]) Writer[T]

MultiWriter creates a writer that duplicates its writes to all the provided writers, similar to the Unix tee(1) command.

Each write is written to each listed writer, one at a time. If a listed writer returns an error, that overall write operation stops and returns the error; it does not continue down the list.

Example
package main

import (
	"bytes"
	"fmt"
	"log"
	"strings"

	"github.com/zalgonoise/gio"
)

func main() {
	r := strings.NewReader("some io.Reader stream to be read\n")

	var buf1, buf2 bytes.Buffer
	w := gio.MultiWriter[byte](&buf1, &buf2)

	if _, err := gio.Copy[byte](w, r); err != nil {
		log.Fatal(err)
	}

	fmt.Print(buf1.String())
	fmt.Print(buf2.String())

}
Output:

some io.Reader stream to be read
some io.Reader stream to be read

type WriterAt

type WriterAt[T any] interface {
	WriteAt(p []T, off int64) (n int, err error)
}

WriterAt is the interface that wraps the basic WriteAt method.

WriteAt writes len(p) T items from p to the underlying data stream at offset off. It returns the number of T items written from p (0 <= n <= len(p)) and any error encountered that caused the write to stop early. WriteAt must return a non-nil error if it returns n < len(p).

If WriteAt is writing to a destination with a seek offset, WriteAt should not affect nor be affected by the underlying seek offset.

Clients of WriteAt can execute parallel WriteAt calls on the same destination if the ranges do not overlap.

Implementations must not retain p.

type WriterTo

type WriterTo[T any] interface {
	WriteTo(w Writer[T]) (n int64, err error)
}

WriterTo is the interface that wraps the WriteTo method.

WriteTo writes data to w until there's no more data to write or when an error occurs. The return value n is the number of T items written. Any error encountered during the write is also returned.

The Copy function uses WriterTo if available.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL