The .NET ecosystem is vast and continuously evolving, introducing new interfaces and methods to make developers’ lives easier. One such interface is ISpanFormattable. Introduced as part of the System namespace, this interface provides a way to format a value as a span of characters.

Before we talk about ISpanFormattable, let’s first analyze IFormattable which is the interface that is inherited by ISpanFormattable.

IFormattable

The IFormattable interface is a standard interface in the .NET Framework that allows an object to provide a custom string representation of its value. It’s particularly useful for formatting values in a manner that’s sensitive to the cultural settings of the user’s environment.

Key Method:

The primary method in the IFormattable interface is:

string ToString(string? format, IFormatProvider? formatProvider);
  • format: A string that specifies the desired format. If null or empty, the object should use its default format.
  • formatProvider: An object that provides culture-specific formatting information. This is typically an instance of CultureInfo or NumberFormatInfo.

Usage:

Classes that represent numeric values, dates, times, and other types that have multiple string representations often implement IFormattable. For example, the DateTime and Double types in .NET implement this interface to provide custom string representations based on format strings.

How ISpanFormattable Works

The ISpanFormattable interface defines a single method, TryFormat, which attempts to format the current instance into the provided span of characters. If the operation is successful, it returns true; otherwise, it returns false. This method provides a more memory-efficient way to format values compared to traditional methods that return a new string.

bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format = default, IFormatProvider? provider = null);

Let’s see an example:

using System;
using System.Globalization;

public class Person : ISpanFormattable
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format = default, IFormatProvider provider = null)
    {
        int requiredLength;
        if (format.IsEmpty || format.SequenceEqual("F".AsSpan()))
        {
            requiredLength = FirstName.Length + 1 + LastName.Length;
            if (destination.Length < requiredLength)
            {
                charsWritten = 0;
                return false;
            }

            FirstName.AsSpan().CopyTo(destination);
            destination[FirstName.Length] = ' ';
            LastName.AsSpan().CopyTo(destination.Slice(FirstName.Length + 1));

            charsWritten = requiredLength;
            return true;
        }
        else if (format.SequenceEqual("I".AsSpan()))
        {
            requiredLength = 4; // e.g., "J.D."
            if (destination.Length < requiredLength)
            {
                charsWritten = 0;
                return false;
            }

            // Format as "F.L."
            destination[0] = FirstName[0];
            destination[1] = '.';
            destination[2] = LastName[0];
            destination[3] = '.';

            charsWritten = requiredLength;
            return true;
        }
        else
        {
            throw new FormatException($"The format of '{format.ToString()}' is invalid.");
        }
    }

    public string ToString(string format, IFormatProvider formatProvider)
    {
        if (string.IsNullOrEmpty(format) || format == "F")
        {
            return $"{FirstName} {LastName}";
        }
        else if (format == "I")
        {
            return $"{FirstName[0]}.{LastName[0]}.";
        }
        else
        {
            throw new FormatException($"The format of '{format}' is invalid.");
        }
    }
}


// Usage:
var person = new Person("John", "Doe");
Span<char> buffer = new char[50];

if (person.TryFormat(buffer, out int charsWritten, "F".AsSpan()))
{
    Console.WriteLine(new string(buffer.Slice(0, charsWritten)));  // Outputs: John Doe
}

if (person.TryFormat(buffer, out charsWritten, "I".AsSpan()))
{
    Console.WriteLine(new string(buffer.Slice(0, charsWritten)));  // Outputs: J.D.
}

  • The TryFormat method and the ToString method both support the “F” and “I” formats.
  • The “F” format (or default) outputs the full name.
  • The “I” format outputs the initials of the person.

Benefits and Potential Use-Cases

  • Performance: By avoiding intermediate string allocations, applications can achieve better performance, especially in scenarios where formatting is done frequently.
  • Flexibility: Developers have more control over memory usage and can decide where the formatted value should be stored.
  • Use-Cases: This interface is particularly beneficial in high-performance applications, such as real-time data processing systems, game engines, and any scenario where minimizing memory allocations is crucial.

Conclusion

The ISpanFormattable interface is a testament to .NET’s commitment to performance and efficiency. By understanding and leveraging this interface, developers can write more performant and memory-efficient applications. As the .NET ecosystem continues to evolve, it’s essential to stay updated with such advancements to harness their full potential.

2 responses to “How ISpanFormattable works in .NET”

  1. That’s actually a terrible implementation of TryFormat. With output = $"{FirstName} {LastName}".AsSpan() you are allocating a string while the whole point of TryFormat is to avoid memory allocation to save on performance.

    Liked by 1 person

    1. Thanks for the finding, I fixed it now.

      Like

Leave a comment

Trending