0

Is it possible in C# to have the string formatting of an interpolated string to be performed only if it is required?

Take the following example code:

public class Program
{
    private static bool isLoggingEnabled = true;

    public static void Main()
    {
        string name = "John";
        Log("Hello {0}!", name);
        Log($"Hello {name}!");
    }

    private static void Log(string formatString, params object?[] formatStringArguments)
    {
        if (isLoggingEnabled)
        {
            Console.WriteLine(string.Format(formatString, formatStringArguments));
        }
    }
}

The first Log() method call passes a format string, and so the string formatting will be performed only if the isLoggingEnabled flag is true.

However, in the second method call that uses an interpolated string, the string formatting is performed already before the method is called, even when isLoggingEnabled is false. It would be nice to have the string formatting be done conditionally and still benefit from the readability arising from an interpolated string.

Is it possible to annotate the formatString and formatStringArguments parameters of the Log() method to indicate that they represent a format string so that the call Log($"Hello {name}!") would be compiled into Log("Hello {0}!", name) which would have the desired effect?

4
  • 1
    why do you want that? Do you care for the nano-seconds it takes to create the string, when you don't need it? What is your problem with the interpolation? Commented Feb 20 at 10:25
  • 5
    You should probably implement an interpolated string handler and create a new overload of the Log method. See here: Implement the handler pattern Commented Feb 20 at 10:27
  • The JIT may well inline your Log method, see that isLoggingEnabled is false and not bother constructing the formatted string, so there will be no performance hit in that case. As ever with performance, profile to find your hotspots and only worry about those. Commented Feb 20 at 11:53
  • Is there a reason you want to build this yourself? Logging in C# points towards several options and, so far as I'm aware, they already perform such optimizations (as well as e.g. allowing more granularity to logging than just true or false) Commented Feb 20 at 14:06

2 Answers 2

2

What you are after can exactly be achieved with FormattableString like:

public void Log(FormattableString str) {
    // JUST DEBUGGING
    Console.WriteLine(str?.Format); // Hello {0}
    Console.WriteLine(str?.GetArguments().FirstOrDefault()); // John
    // formatting is not done (calling ToString)
    // we just have the compiler extract a custom
    // Format string and pass us a list of arguments

    if (IsLoggingEnabled) {
        Console.WriteLine(str.ToString());
    }
}

The issue is that the compiler would prefer the overload of your other method:

Log(string formatString, params object?[] formatStringArguments)

if they are named the same. More info in this question.

So, you either have method with another name such as LogWithFormattable or you can implement a custom interpolation handler (if on .NET 6+) which has priority over string when overloads are done. A use case similar to yours was alluded to in this this blog post.

If we adapt slightly the code from the tutorial that was mentioned in the comments, we would get something close to what you are after:

public class Logger {
    public bool IsLoggingEnabled { get; set; } = false;

    // [InterpolatedStringHandlerArgument("")]
    // is for the this reference of Logger
    // required by the LogInterpolatedStringHandler
    public void Log([InterpolatedStringHandlerArgument("")] LogInterpolatedStringHandler builder) {
        if (IsLoggingEnabled) {
            Console.WriteLine(builder.GetFormattedText());
        } else {
            // for debugging only
            // will be null with current logic
            Console.WriteLine(builder.GetFormattedText()); // null
        }
    }

    public void Log(string formatString, params object?[] formatStringArguments) {
        if (IsLoggingEnabled) {
            Console.WriteLine(string.Format(formatString, formatStringArguments));
        }
    }
}

[InterpolatedStringHandler]
public struct LogInterpolatedStringHandler {
    // Storage for the built-up string
    StringBuilder builder;

    public LogInterpolatedStringHandler(int literalLength, int formattedCount,
    Logger logger,
    out bool isEnabled) {
        isEnabled = logger.IsLoggingEnabled;
        builder = isEnabled ? new StringBuilder(literalLength) : default!;
    }

    public void AppendLiteral(string s) {
        builder.Append(s);
    }
    public void AppendFormatted<T>(T t) {
        builder.Append(t?.ToString());
    }

    internal string GetFormattedText() => builder?.ToString();
}

Simple test code:

var logger = new Logger();
logger.IsLoggingEnabled = true;

string name = "John";
logger.Log($"Hello {name}!"); // "Hello John"

what the code above translates to (i.e. the compiler magic of it all):

var logger = new Logger();
logger.IsLoggingEnabled = true;

string name = "John";

// logger.Log($"Hello {name}!"); // "Hello John"
// translates to:
bool isEnabled;
LogInterpolatedStringHandler builder = new LogInterpolatedStringHandler(7, 1, logger, out isEnabled);
// we have set the isEnabled to the value of 
// logger.IsLoggingEnabled in the constructor

// we only proceed with formatting if
// isEnabled = true
if (isEnabled)
{
    builder.AppendLiteral("Hello ");
    builder.AppendFormatted(name);
    builder.AppendLiteral("!");
}
// this finally calls our Log method
logger.Log(builder);
Sign up to request clarification or add additional context in comments.

Comments

1

One approach is to add an overload that takes a delegate that produces a string instead:

 Log(() => $"Hello {name}!");

private static void Log(Func<string> logMessageGenerator)
{
     if (isLoggingEnabled)
     {
          Console.WriteLine(logMessageGenerator());
     }
}

The main benefit of this is that it can avoid the creation of expensive parameters for logging, like Log(() => $"Hello {GetNameFromDatabase()}!").

Note that for the vast majority of logging the small amount of time used to interpolate a string will not matter. Where you want to be careful is with tight loops. It is a good idea to use profiling tools to find the places where logging have an actual performance impact, and address these specific places.

I would also consider using a logging framework, since these have overloads to address the most common use cases.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.