dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.42k stars 4.76k forks source link

[API Proposal]: Boolean and String should implement IFormattable #88275

Open rickbrew opened 1 year ago

rickbrew commented 1 year ago

Background and motivation

This request is similar to #78523 and #78842. System.String and System.Boolean recently got support for IParsable<T> in #82836 for .NET 8 thanks to @tannergooding, but IFormattable would help to complete the picture here.

IFormattable gives us the ToString() overload that takes a format and, more importantly, an IFormatProvider. The latter is useful in round-tripping scenarios so that CultureInfo.InvariantCulture can be used to ensure data does not change meaning across app sessions, across the wire, etc. Obviously String and Boolean do not need this parameter since their output is culture invariant anyway, but having the interfaces implemented there would enable these types to be used in generic contexts that make use of both IParsable<T> and IFormattable.

For my purposes, I use these interfaces for persisting app-wide settings, which are generally strings, booleans, enums, and sometimes points or rectangles. It would be nice to reduce the types that need special handling, or special wrapper structs, by two.

The interfaces can/should be implemented explicitly, as their purpose is to enable use in generic contexts.

String and Boolean both already implement ToString(IFormatProvider?), they just need ToString(string?, IFormatProvider?) to make them usable in generic contexts.

API Proposal

namespace System;

public partial struct Boolean : IFormattable
{
    string IFormattable.ToString(string? format, IFormatProvider? formatProvider)
    {
        return ToString(formatProvider);
    }
}

public partial class String : IFormattable
{
    string IFormattable.ToString(string? format, IFormatProvider? formatProvider)
    {
        return ToString(formatProvider);
    }
}

API Usage

In my application, settings are essentially (string name, string value) tuples. Non-string types are handled by converting to/from strings.

Here's a distilled pseudo-example of how it works, including how I make use of IFormattable + IParsable<T> when possible:

public sealed class AppSettings
{
    public void Set(string name, string value);

    public bool TryGet(string name, [NotNullWhen(true)] out string? value);
}

And here would be helper methods for working with non-string types:

public static class AppSettingsExtensions
{
    public static void Set<T>(this AppSettings settings, string name, in T value) 
        where T : IFormattable, IParsable<T>
    {
        settings.Set(name, value.ToString(null, CultureInfo.InvariantCulture));        
    }

    public static bool TryGet<T>(this AppSettings settings, string name, [MaybeNullWhen(false)] out T value)
        where T : IFormattable, IParsable<T>
    {
        if (settings.TryGet(name, out string? valueStr) &&
            T.TryParse(valueStr, CultureInfo.InvariantCulture, out T value))
        {
            return true;
        }
        else
        {
            value = default;
            return false;
        }
    }
}

Alternative Designs

No response

Risks

None that I can see. The new interfaces can be implemented explicitly and can be expressed in terms of methods that are already implemented on these types.

Sergio0694 commented 1 year ago

If we're going to add IFormattable, it seems like at that point it would be better to have them implement ISpanFormattable too? Not doing so would feel a bit weird — you'd get the same functionality but just be locked to the more inefficient version of it (depending on context) 🤔

rickbrew commented 1 year ago

Yes by IFormattable I really mean ISpanFormattable, which also implements IFormattable :)

vcsjones commented 1 year ago

Boolean is being tracked at https://github.com/dotnet/runtime/issues/67388 and has further design discussion.

ghost commented 1 year ago

Tagging subscribers to this area: @dotnet/area-system-runtime See info in area-owners.md if you want to be subscribed.

Issue Details
### Background and motivation This request is similar to #78523 and #78842. `System.String` and `System.Boolean` recently got support for `IParsable` in #82836 for .NET 8 thanks to @tannergooding, but `IFormattable` would help to complete the picture here. `IFormattable` gives us the `ToString()` overload that takes a `format` and, more importantly, an `IFormatProvider`. The latter is useful in round-tripping scenarios so that `CultureInfo.InvariantCulture` can be used to ensure data does not change meaning across app sessions, across the wire, etc. Obviously `String` and `Boolean` do not need this parameter since their output is culture invariant anyway, but having the interfaces implemented there would enable these types to be used in generic contexts that make use of both `IParsable` and `IFormattable`. For my purposes, I use these interfaces for persisting app-wide settings, which are generally strings, booleans, enums, and sometimes points or rectangles. It would be nice to reduce the types that need special handling, or special wrapper structs, by two. The interfaces can/should be implemented explicitly, as their purpose is to enable use in generic contexts. `String` and `Boolean` both already implement `ToString(IFormatProvider?)`, they just need `ToString(string?, IFormatProvider?)` to make them usable in generic contexts. ### API Proposal ```csharp namespace System; public partial struct Boolean : IFormattable { string IFormattable.ToString(string? format, IFormatProvider? formatProvider) { return ToString(formatProvider); } } public partial class String : IFormattable { string IFormattable.ToString(string? format, IFormatProvider? formatProvider) { return ToString(formatProvider); } } ``` ### API Usage In my application, _settings_ are essentially `(string name, string value)` tuples. Non-string types are handled by converting to/from strings. Here's a distilled pseudo-example of how it works, including how I make use of `IFormattable` + `IParsable` when possible: ```cs public sealed class AppSettings { public void Set(string name, string value); public bool TryGet(string name, [NotNullWhen(true)] out string? value); } ``` And here would be helper methods for working with non-`string` types: ```cs public static class AppSettingsExtensions { public static void Set(this AppSettings settings, string name, in T value) where T : IFormattable, IParsable { settings.Set(name, value.ToString(null, CultureInfo.InvariantCulture)); } public static bool TryGet(this AppSettings settings, string name, [MaybeNullWhen(false)] out T value) where T : IFormattable, IParsable { if (settings.TryGet(name, out string? valueStr) && T.TryParse(valueStr, CultureInfo.InvariantCulture, out T value)) { return true; } else { value = default; return false; } } } ``` ### Alternative Designs _No response_ ### Risks None that I can see. The new interfaces can be implemented explicitly and can be expressed in terms of methods that are already implemented on these types.
Author: rickbrew
Assignees: -
Labels: `api-suggestion`, `area-System.Runtime`, `untriaged`
Milestone: -
stephentoub commented 1 year ago

ISpanFormattable isn't actually better if getting the formatted result string is cheap. If the destination to write to isn't large enough and the caller needs to grow a buffer, with the string it knows a minimum to grow by, whereas with ISpanFormattable it doesn't. I don't see a good reason to implement ISpanFormattable on string.

Constraining to IFormattable is also questionable IMO, given that every object already has and can override ToString, and this is then shutting out types that have perfectly valid ToStrings and don't implement IFormattable because they're not sensitive to culture or have formatting options. You can type test for IFormattable and use it if it's available, otherwise use ToString, no constraint necessary, and then no benefit to adding this to string.

mariusz96 commented 1 year ago

👍 for IFormattable booleans - it's a relatively easy primitive to miss when working with IFormattable in reflection/generic/abstract contexts and it's a bit special in that pretty much all the other primitives already implement this standard formatting interface.