Get the original name of a variable passed as a pa

2020-06-25 05:15发布

问题:

To be clear, this is Not a duplicate of this question. Obviously, I can use the nameof operator to get the name of a variable or a parameter; I know that. But is there a way I can get the original name of a variable that's passed to a method? Currently, I have to do it like this:

static void Foo(string someVariable, string variableName)
{
    if (!FulfilsCondition(someVariable))
        Console.WriteLine($"{variableName} is bad!");

    // More code
}

And I call it like this:

string bar = string.Empty;
Foo(bar, nameof(bar));    // Or...
//Foo(bar, "bar");

But I'm looking for a way to avoid repeatedly providing the name of the variable and, instead, use something like:

Foo(bar);

Where Foo, in this case, would be:

static void Foo(string someVariable)
{
    string variableName = GetOriginalVariableName(someVariable);
    //  Is this possible? ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ 
    if (!FulfilsCondition(someVariable))
        Console.WriteLine($"{variableName} is bad!");

    // More code
}

Is something like this achievable in .NET?


Update:

I didn't think of the possibility that what gets passed to Foo could be an expression and not a variable as others have suggested in the comments. Thinking about it now, it doesn't seem to be possible to achieve what _unless I can (somehow) guarantee that there will always be a variable? Sounds like a long shot, TBH, but maybe there's a solution there.

Update #2:

People asked about what I'm actually trying to achieve. Well, it's very similar to the first method above but here's the actual method I'm using if that's going to help:

static bool ExceedsLimit(string s, int maxLength, string variableName,
                         out string errorMessage)
{
    if (s.Length > maxLength)
    {
        errorMessage = $"'{variableName}' must be {maxLength} characters at most.";
        return true;
    }

    errorMessage = null;
    return false;
}

And I'm using it in something like this:

static bool TestMethod(out bool failReason)
{
    if (ExceedsLimit(obj.Prop1, 100, nameof(obj.Prop1), out failReason)) return false;
    if (ExceedsLimit(obj.Prop2, 50, nameof(obj.Prop2), out failReason)) return false;
    if (ExceedsLimit(obj.Prop3, 80, nameof(obj.Prop3), out failReason)) return false;
    // ...
}

But I'm looking for a way to avoid repeatedly providing the name of the variable.

回答1:

What you are looking for is going to work much slower than passing param name additionally.

But workarounds are possible. I went crazy about your problem and found out something. It has restrictions. Such as dealing with local variables only. (But can be extended to solve other cases). And it needs pdb files and ildasm tool. (It seemed to be the simplest way to get IL, but maybe it can be obtained with framework functionality). And it's terribly slow. But it works) Just call ParamNameHelper.GetOriginalVariableName(string paramName).

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;

namespace TestParamHelper
{
    class Program
    {
        static void Main(string[] args)
        {
            new CallingClass().CallTargetMethod();
        }
    }

    public class CallingClass
    {
        public void CallTargetMethod()
        {
            var s = "str";
            var i = 5;
            new TargetClass().TargetMethod(s, i);
        }
    }

    public class TargetClass
    {
        public void TargetMethod(string strArg, int intArg)
        {
            var paramName = nameof(strArg);

            // HERE IT IS!!!
            var originalName = ParamNameHelper.GetOriginalVariableName(paramName);

            Console.WriteLine($"{originalName} is passed as {paramName}");
        }
    }

    public static class ParamNameHelper
    {
        public static string GetOriginalVariableName(string paramName)
        {
            var stackTrace = new StackTrace(true);

            var targetMethod = stackTrace.GetFrame(1).GetMethod();
            var paramIndex = targetMethod.GetParameters().ToList().FindIndex(p => p.Name.Equals(paramName));

            var callingMethod = stackTrace.GetFrame(2).GetMethod();
            var il = callingMethod.GetMethodBodyIL();

            var localIndex = il
                .TakeWhile(s => !s.Contains($"{targetMethod.DeclaringType.FullName}::{targetMethod.Name}"))
                .Reverse()
                .TakeWhile(s => s.Contains("ldloc"))
                .Reverse()
                .ElementAt(paramIndex)
                .Split('.')
                .Last();

            return il
                .SkipWhile(s => !s.Contains("locals init"))
                .TakeWhile(s => s.Contains(",") || s.Contains(")"))
                .First(s => s.Contains($"[{localIndex}]"))
                .Replace(")", "")
                .Replace(",", "")
                .Split(' ')
                .Last();
        }
    }

    internal static class MethodBaseExtensions
    {
        // improve providing location, may be via config
        private static readonly string ildasmLocation = Path.GetFullPath(@"C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.7.2 Tools\ildasm.exe");

        internal static IEnumerable<string> GetMethodBodyIL(this MethodBase method)
        {
            var assemblyLocation = method.DeclaringType.Assembly.Location;
            var ilLocation = $"{assemblyLocation}.il";

            Process.Start(new ProcessStartInfo(ildasmLocation, $"{assemblyLocation} /output:{ilLocation}") { UseShellExecute = false })
                .WaitForExit();

            var il = File.ReadAllLines(ilLocation)
                .SkipWhile(s => !s.Contains(method.Name))
                .Skip(2)
                .TakeWhile(s => !s.Contains($"end of method {method.DeclaringType.Name}::{method.Name}"));

            File.Delete(ilLocation);

            return il;
        }
    }
}

Output: s is passed as strArg



回答2:

No, It's not possible because a method has no way of knowing what the names of it's arguments where. Aka Foo has no way of knowing if it was called as Foo(bar) or Foo(baz). What you cold do is use CallerMemberName to get the name of the method it was called from. For example:

static void Foo(string someVariable, [CallerMemberName] string methodName = "")
{
    if (!FulfilsCondition(someVariable))
        Console.WriteLine($"{methodName} passed a bad paramter!");

    // More code
}

static void BadMethod()
{
    string wrong = "";
    Foo(wrong);
}

Would print:

BadMethod passed a bad paramter!


回答3:

You can use Mono.Cecil for that purpose. You also need MonoCecilReader and some other types from this answer and .pdb files to get local variables' names.

Ammend MethodDefinitionExtensions with

public static Instruction GetInstruction(this MethodDefinition method, int offset) =>
    method
    .Body
    .Instructions
    .SingleOrDefault(i => i.Offset == offset);

Create ResolvedArgument class:

public class ResolvedArgument
{
    public string Argument { get; }
    public string Parameter { get; }

    public ResolvedArgument(string argument, string parameter) =>
        (Argument, Parameter) = (argument, parameter);

    public override string ToString() =>
        $"'{Argument}' passed for '{Parameter}'";
}

Then create static class VariableHelper with methods:

public static ResolvedArgument ResolveArgument(object parameter)
{
    var trace = new StackTrace();

    var frame = trace.GetFrame(1);
    var method = frame.GetMethod().GetMethodDefinition();
    var arg = GetParameter(frame, method);

    frame = trace.GetFrame(2);
    return GetPassedValue(frame, method, arg);
}

Where GetParameter is:

private static ParameterDefinition GetParameter(StackFrame frame, MethodDefinition method)
{
    var instruction = method.GetInstruction(frame.GetILOffset());

    if (instruction.IsBoxing())
        instruction = instruction.Previous;

    if (!instruction.IsLoadArg())
        throw new NotSupportedException($"Attempt to handle {instruction.OpCode}");

    if (instruction.OpCode.Code == Code.Ldarg_S)
        return (ParameterDefinition)instruction.Operand;

    var index = instruction.GetArgIndex(!method.IsStatic);
    return method.Parameters[index];
}

and GetPassedValue is:

private static ResolvedArgument GetPassedValue(StackFrame frame, MethodDefinition method, ParameterDefinition parameter)
{
    var info = frame.GetMethod();
    var caller = info.GetMethodDefinition();
    var instruction = caller.GetInstruction(frame.GetILOffset());
    while (instruction != null)
    {
        if (instruction.IsCall() &&
            instruction.Operand is MethodDefinition md &&
            md.FullName.Equals(method.FullName))
                break;
        instruction = instruction.Previous;
    }

    if (instruction == null)
        throw new Exception("Not supposed to get here.");

    var il = caller
        .Body
        .Instructions
        .TakeWhile(i => i.Offset != instruction.Offset)
        .Reverse()
        .Where(i => !i.IsBoxing() && (caller.IsStatic || i.OpCode.Code != Code.Ldarg_0))
        .TakeWhile(i =>i.IsLoad())
        .Reverse()
        .ToList();

    if (il.Count != method.Parameters.Count)
        throw new NotSupportedException("Possible attempt to pass an expression");

    instruction = il[parameter.Index];

    var name = "<failed to resolve>";

    if (instruction.IsLoadArg())
    {
        var index = instruction.GetArgIndex(!caller.IsStatic);
        name = caller.Parameters.Single(p => p.Index == index).Name;
    }

    if (instruction.IsLoadField())
        name = ((FieldDefinition)instruction.Operand).Name;

    if (instruction.IsLoadLoc())
    {
        var index = instruction.GetLocIndex();
        var locals = new MonoCecilReader().Read(info);
        name = locals.Single(loc => loc.Index == index).Name;
    }

    return new ResolvedArgument(name, parameter.Name);
}

Extension methods for Instruction are:

internal static class InstructionExtensions
{
    public static bool IsCall(this Instruction instruction)
    {
        var code = instruction.OpCode.Code;

        return code == Code.Call ||
               code == Code.Callvirt;
    }

    public static bool IsBoxing(this Instruction instruction) =>
        instruction.OpCode.Code == Code.Box;

    public static bool IsLoadArg(this Instruction instruction)
    {
        var code = instruction.OpCode.Code;

        return code == Code.Ldarg_0 ||
               code == Code.Ldarg_1 ||
               code == Code.Ldarg_2 ||
               code == Code.Ldarg_3 ||
               code == Code.Ldarg_S;
    }

    public static bool IsLoadLoc(this Instruction instruction)
    {
        var code = instruction.OpCode.Code;

        return code == Code.Ldloc_0 ||
               code == Code.Ldloc_1 ||
               code == Code.Ldloc_2 ||
               code == Code.Ldloc_3 ||
               code == Code.Ldloc_S;
    }

    public static bool IsLoadField(this Instruction instruction)
    {
        var code = instruction.OpCode.Code;

        return code == Code.Ldfld ||
               code == Code.Ldsfld;
    }

    public static int GetArgIndex(this Instruction instruction, bool isInstance)
    {
        if (instruction.OpCode.Code == Code.Ldarg_S)
            return ((ParameterDefinition)instruction.Operand).Index;

        var index = -1;
        var code = instruction.OpCode.Code;
        if (code == Code.Ldarg_0)
            index = 0;
        else if (code == Code.Ldarg_1)
            index = 1;
        else if (code == Code.Ldarg_2)
            index = 2;
        else if (code == Code.Ldarg_3)
            index = 3;

        if (index != -1 && isInstance)
            index--;

        return index;
    }

    public static int GetLocIndex(this Instruction instruction)
    {
        if (instruction.OpCode.Code == Code.Ldloc_S)
            return ((VariableDefinition)instruction.Operand).Index;

        var code = instruction.OpCode.Code;

        if (code == Code.Ldloc_0)
           return 0;

        if (code == Code.Ldloc_1)
            return 1;

        if (code == Code.Ldloc_2)
            return 2;

        if (code == Code.Ldloc_3)
            return 3;

        return -1;
    }

    public static bool IsLoad(this Instruction instruction) =>
        instruction.IsLoadArg() ||
        instruction.IsLoadLoc() ||
        instruction.IsLoadField();
}

Usage:

class Program
{
    private static readonly Guid sFld1 = default(Guid);
    private readonly DateTime iFld1 = default(DateTime);
    private static readonly Guid sFld2 = default(Guid);
    private readonly DateTime iFld2 = default(DateTime);

    static void Main(string[] args)
    {
        new Program().Run("_1", "_2");
    }

    private void Run(string arg1, string arg2)
    {
        int loc1 = 42;
        int loc2 = 24;
        Console.WriteLine("\tFirst call");
        Method(p1: loc1, p2: arg1, p3: sFld1, p4: iFld1);
        Console.WriteLine("\tSecond call");
        Method(p1: loc2, p2: arg2, p3: sFld2, p4: iFld2);
    }

    private void Method(int p1, string p2, object p3, DateTime p4)
    {
        Console.WriteLine(VariableHelper.ResolveArgument(p1));
        Console.WriteLine(VariableHelper.ResolveArgument(p2));
        Console.WriteLine(VariableHelper.ResolveArgument(p3));
        Console.WriteLine(VariableHelper.ResolveArgument(p4));
    }
}

Gives:

        First call
'loc1' passed for 'p1'
'arg1' passed for 'p2'
'sFld1' passed for 'p3'
'iFld1' passed for 'p4'
        Second call
'loc2' passed for 'p1'
'arg2' passed for 'p2'
'sFld2' passed for 'p3'
'iFld2' passed for 'p4'

The above solution is mostly a demonstration of possibility. It works much slower than simply passing a variable's name.