3

Is there a shorter way to print out a variable name and its value in C#? What i do now is:

int myvar = 42;
Console.WritelLine($"{nameof(myvar)}={myvar}"); // or any other log function
//myvar=42

(I have been using Python a lot lately and really like pythons f"{myvar=}", which does exactly that.)

Thanks for your help

tturbo
  • 680
  • 5
  • 16
  • Where is the variable coming from? Here, you could just hard code `"myvar"`. I'm wondering if you could use reflection or something. But it isn't clear how you will work with a variable and not know the name of that variable. – Jonathan Wood Nov 14 '22 at 17:40
  • @JonathanWood Reflection won't include the names of locals (at least, not unless it's a Debug build with symbols available). – Dai Nov 14 '22 at 17:49
  • @Dai: I pointed out that he already has the name if it's a local variable. That's exactly why I asked where the variable is coming from. – Jonathan Wood Nov 14 '22 at 18:01
  • @JonathanWood why should hard code be better? `nameof(myvar)` is refactoring save. – tturbo Nov 15 '22 at 13:10

1 Answers1

2

Yes-and-No.

No, you can't capture local variable names as simply as how Python does it.

...but it is possible by using a helper method with CallerArgumentExpression - this does require C# 10.0 and .NET 6+ - you also need to add [assembly: EnableCallerArgumentExpression] in your AssemblyInfo.cs.

Like so:

public static class MyExtensions
{
    public static String Dump<T>( this T value, [CallerArgumentExpression(nameof(value))] String? name = null )
    {
        String valueAsText;
        if( value == null )
        {
            valueAsText = "null";
        }
        else if( value is IEnumerable e ) // Can't use IEnumerable<T> (unless you use MethodInfo.MakeGenericMethod, which is overkill)
        {
            valueAsText = "{ " + e.Cast<Object?>().Select( i => i?.ToString() ).StringJoin() + " }";
        }
        else
        {
            valueAsText = '"' + value.ToString() + '"';
        }

        name = name ?? "(Unnamed expression)";

        return name + " := " + valueAsText;
    }

    public static String StringJoin( this IEnumerable<String?> collection, String separator = ", " )
    {
        return String.Join( separator: separator, collection.Select( s => s is null ? "null" : ( '"' + s + '"' ) ) );
    }
}

The Dump<T> method above is generic (over T) instead of using this Object? value to avoid unnecessary boxing of value when T is a value-type.

Used like so:

int myvar = 42;
Console.WriteLine( myvar.Dump() );
// myvar := "42"

Int32?[] arr = new[] { 1, 2, 3, (Int32?)null, 5 };
Console.WriteLine( arr.Dump2() );
// arr := { "1", "2", "3", null, "5" }

Screenshot proof:

enter image description here

(I had to name it Dump2 in the above screenshot because Dump is already defined by Linqpad).

Dai
  • 141,631
  • 28
  • 261
  • 374
  • This seems like a lot of work to make this an extension method, what happens if you were to call this on an array, list, etc? – Ryan Wilson Nov 14 '22 at 17:24
  • @RyanWilson _"This seems like a lot of work to make this an extension method"_ - as opposed to? – Dai Nov 14 '22 at 17:26
  • @RyanWilson The problem with using `Object` is it will box value-types, which is undesirable. Also, your code will throw if `o` is null. Also, `nameof(o)` will always return `"o"` - which is not what the OP wants. – Dai Nov 14 '22 at 17:28
  • @RyanWilson _"what happens if you were to call this on an array, list, etc?"_ - I've updated my answer to handle `IEnumerable` types (including `Array`, `List`, etc) - though in that case it _does_ box collection members - but _it is still possible_ to avoid that boxixng with _yet even more complexity_ but I don't want to over-complicate my answer. – Dai Nov 14 '22 at 17:36
  • @Dai your code boxes anyway and does so at least twice :) Namely if you compare a generic with `null` you'll get a box op, plus of course that object cast in the IEnumerable code route as you noted. – Luke Briggs Nov 14 '22 at 18:46
  • @LukeBriggs No, comparing generic-typed value-type params with `== null` is elided by the JIT: https://stackoverflow.com/questions/48685875/does-performing-a-null-check-on-a-generic-cause-boxing and https://ericlippert.com/2013/01/24/five-dollar-words-for-programmers-elision/ - though the second `is` does box it - I’ll fix that now… hang on. – Dai Nov 14 '22 at 18:55
  • @Dai Ah great thanks, I knew the box opcode was in the IL but not that the JIT specifically eliminated them, nice; even so though there are plenty of other allocations going on so the point I was more trying to make is I'd doubt eliminating them in a use case like this one matters much at all (particularly if they are present anyway in those hidden spots). – Luke Briggs Nov 14 '22 at 19:06
  • 1
    @LukeBriggs I took a stab at making this `Dump(...)` method _completely_ free of boxing/unboxing - including tricks like `Reflection. Emit` - and reducing string concatenation operations too - but then things snowballed - by the time I added `Span` parameters for zero-allocation strings I realised I was getting ahead of myself - and I'm procrastinating on my actual real day-job so I must end it here, sorry – Dai Nov 14 '22 at 21:29
  • Oh gosh I'm sorry lol, I can relate to that one too! Thank you for caring about performance - it's that drive which has allowed things like span to appear in C# and it become an all round wonderful language and the kind of throughputs that are now possible are amazing these days. – Luke Briggs Nov 14 '22 at 22:22
  • @Dai: Thanks for your suggestion, I was wondering if I was missing something that is already in the language. But if I ever get to the point where I need to do this a lot, I will take your approach. – tturbo Nov 15 '22 at 13:22