Getting the calling variable name of a parameter

末鹿安然 提交于 2019-12-22 09:06:38

问题


In relation to the question Get the name of parameters from a calling method and Finding the Variable Name passed to a Function in C# I'm still looking for a way to define theWhatDoesTheAnimalSay_WANTED method: I want to know the name of the variable that's used as a parameter:

public class Farm
{
    public readonly string Cow = "Muuuuhh";

    public string Cat { get; set; }

    public void MainFunction()
    {
        var dog = "WauWau";
        var kiwi = new Bird("QueeeekQueeek");

        Cat = "Miiiaaauuuu";

        // This one works but looks kinda ugly and is cumbersome when used
        WhatDoesTheAnimalSay(nameof(dog), dog);
        WhatDoesTheAnimalSay(nameof(Cow), Cow);
        WhatDoesTheAnimalSay(nameof(Cat), Cat);
        WhatDoesTheAnimalSay(nameof(kiwi), kiwi);

        WhatDoesTheAnimalSay_WRONG1(dog);
        WhatDoesTheAnimalSay_WRONG2(dog);

        WhatDoesTheAnimalSay_WANTED(dog);
        WhatDoesTheAnimalSay_WANTED(Cow);
    }

    public void WhatDoesTheAnimalSay<T>(string name, T says)
    {
        MessageBox.Show("The " + name + " says: " + says);
        // Shows i.e.: The dog says: WauWau
    }

    public void WhatDoesTheAnimalSay_WRONG1<T>(T says)
    {
        MessageBox.Show("The " + nameof(says) + " says: " + says);
        // Shows: The says says: WauWau
    }

    public void WhatDoesTheAnimalSay_WRONG2<T>(T says, [CallerMemberName] string name = null)
    {
        MessageBox.Show("The " + name + " says: " + says);
        // Shows: The MainFunction says: WauWau
    }
    public void WhatDoesTheAnimalSay_WANTED<T>(T says /*, ?????*/)
    {
        MessageBox.Show("The " /*+ ?????*/ + " says: " + says);
        // Shows: The dog says: WauWau
    }
}

// Just here to show it should work with a class as well.
public class Bird
{
    public string Says { get; } //readonly
    public Bird(string says) {
        Says = says;
    }
    public override string ToString()
    {
        return Says;
    }
}

In real life I need this while implementing the IXmlSerializable interface in my classes with custom reader.Read... and writer.Write.... methods.

So, unfortunately, I cannot introduce a new wrapper class, interface or change where the name of the animal or what it says is saved. Meaning it has to work with classes and with a plain string, int, decimal, ... variables, fields or properties as well. In other words (not in a rude way): Don't change how are the animals defined, just change the WhatDoesTheAnimalSay_WANTED method...

EDIT:

As some of you wanted to know a real use case for this example I'm giving you here an idea how I store & read data in an xml-file. The real Database object is of course way bigger and all sub-classes (like Fitter) implement the IXmlSerializable interface as well using the same extensions methods.

    // This is the Database Class which stores all the data
    public class Database : IXmlSerializable
    {
        // The list of all building sites managed
        public List<BuildingSite> BuildingSites { get; set; }

        // The list of all fitters working for the company
        public List<Fitter> Fitters { get; set; }

        private readonly int DatabaseVersion = 1;

        // Write implementation of the IXmlSerializable inteface
        public void WriteXml(XmlWriter writer)
        {
            // Writing all Data into the xml-file
            writer.WriteElementInt(nameof(DatabaseVersion), DatabaseVersion);
            writer.WriteElementList(nameof(BuildingSites), BuildingSites);
            writer.WriteElementList(nameof(Fitters), Fitters);
        }
        public void ReadXml(XmlReader reader)
        {
            // Do the reading here
        }
        public XmlSchema GetSchema() { return null; }

    }

    public class XmlExtensions
    {
        // Writing a list into the xml-file
        public static void WriteElementList<T>(this XmlWriter writer, string elementName, IEnumerable<T> items)
        {
            var list = items is List<T> ? items : items.ToList();

            // The XML-Element should have the name of the variable in Database!!!
            writer.WriteStartElement(elementName);
            writer.WriteAttributeString("count", list.Count().ToString());
            var serializer = new XmlSerializer(typeof(T));
            list.ForEach(o => serializer.Serialize(writer, o, XmlHelper.XmlNamespaces));
            writer.WriteEndElement();
        }
        public static void WriteElementInt(this XmlWriter writer, string elementName, int toWrite)
        {
            // The XMLElement should have the name of the variable in Database!!!
            writer.WriteElementString(elementName, toWrite.ToString(CultureInfo.InvariantCulture));
        }

        // More here....
    }
}

回答1:


You can use a method which is taking an Expression<Func<object>> as parameter:

public void WhatDoesTheAnimalSay_WANTED(Expression<Func<object>> expression)
{
    var body = (MemberExpression)expression.Body;
    var variableName = body.Member.Name;

    var func = expression.Compile();
    var variableValue = func();

    MessageBox.Show("The "+ variableName + " says: " + variableValue);
}

Using this approach gives the ability to process a variety of variables (static members, instance members, parameters, local variables etc.), also properties are possible.

Call it like:

WhatDoesTheAnimalSay_WANTED(() => dog)
WhatDoesTheAnimalSay_WANTED(() => Cow)
WhatDoesTheAnimalSay_WANTED(() => Cat)
WhatDoesTheAnimalSay_WANTED(() => kiwi)

Constants ar not possible, because the compiler will substitute the constant placeholder by it's value, given at compile time:

const string constValue = "Constant Value";

WhatDoesTheAnimalSay_WANTED(() => constValue)

would be transformed to

WhatDoesTheAnimalSay_WANTED(() => "Constant Value")

making the expression.Body of type ConstantExpression, which would yield an exception at runtime.

So you have to be careful what you provide as expression to that method.

Additional Remarks

As you can notice from the comments below, the use of lambda expressions to gather variable names seems controversial.

As @CodeCaster pointed out in one of his comments, there is no officially specified need for the compiler to take the same name of a local variable for the captured member in the anonymous wrapping class.

However, I found this in the remarks of Expression<TDelegate>:

The ability to treat expressions as data structures enables APIs to receive user code in a format that can be inspected, transformed, and processed in a custom manner.

For me this is a sign that the expression trees are exactly designed for purposes like that.

Although it is possible that Microsoft changes this behavior for some reason, there does not seem to be a logical need for doing so. Relying on the principle of least astonishment I'd say it is safe to assume that for an expression from ()=> dog whose Body property is of type MemberExpression, that body.Member.Name resolves to dog.

If it is necessary to have other types for Body also, the method has to be worked out a little bit more. And also it is possible that this will not work in certain circumstances anyways.




回答2:


I'm afraid there currently is no way to achieve what you want. The local variable name is not available to the called function.

There are alternatives, you listed some. Another alternative would be to put the name of the variable as a property inside the class. That would be the better design anyway because data in your application should not depend on variable names. Your application should work the same way if someone renamed all the variables.

Other languages like the C/C++ family uses preprocessor macros for that purpose but they are not available in C#. You could probably achieve this in C++/CLI, but only inside without the option to export such a macro outside your compilation unit and/or assembly.

So No, sorry, even with C# 6 and it's new features, this is not possible. And it should not need to be. Think about your design, you might come up with a better one.



来源:https://stackoverflow.com/questions/37639335/getting-the-calling-variable-name-of-a-parameter

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!