15

Как можно получить имя передаваемой переменной в вызываемом методе?
Только передавать дополнительным параметром?
Может есть какой аналог CallerMemberNameAttribute только для получения имени передаваемой переменной?

На примере:

var variable = "some_value"; // имя переменной nameof(variable)
DoAction(variable);


public void DoAction(string value)
{
    var valueVariableName = ... // хочу получить имя переменной ("variable")
}

Буду благодарен за любые соображения.


Примечание: Изначально задумывалось сделать наподобие

public void DoAction(string value, [PreviousParameterName] string valueParameterName = null)
{
    var valueParameterName = ... // имя переменной (== "variable")
}

Но после уточнения вопроса создания кастомных аттрибутов (отрабатывающих во время компиляции) стало ясно, что это сделать по-простому невозможно. Естественно, использовать рефлексию для решения данного вопроса нецелесообразно (может даже и не получилось бы, я не разбирался).

MihailPw
  • 6,384
  • 5
    а зачем это может понадобиться? и нет, аналога нет – Grundy Jun 30 '17 at 12:51
  • 2
    зачем вообще это может понадобиться? – Ev_Hyper Jun 30 '17 at 12:51
  • Все сугубо для удобства разработки – MihailPw Jun 30 '17 at 12:51
  • 1
    а если вызывать DoAction("some_value"); что вы ожидаете увидеть в таком случае? – Ev_Hyper Jun 30 '17 at 12:53
  • 1
    @Ev_Hyper мне без разницы, хоть пусть эксепшн валится – MihailPw Jun 30 '17 at 12:54
  • 2
    @Grundy По поводу того, зачем может понадобится. Например, если хочется сделать статический метод, бросающий ArgumentNullException (вместо if (...) throw ...). Тогда в него достаточно передавать сам параметр, а не параметр с nameof(параметр). Господа, а за что минусы кидаете человеку? – Vlad Jun 30 '17 at 12:57
  • Держи похожий вопрос на английском SO. Признанным считается ответ, что так нельзя. Однако есть и другие варианты. https://stackoverflow.com/questions/72121/finding-the-variable-name-passed-to-a-function – Никита Васильченко Jun 30 '17 at 13:07
  • @НикитаВасильченко эти способы не совсем то, что мне нужно. Но спасибо :) – MihailPw Jun 30 '17 at 13:15
  • 3
    А что должно произойти, если в функцию будет передано выражение, содержащее несколько переменных? DoAction(s1 + s2), например. – VladD Jun 30 '17 at 13:42
  • @VladD эксепшн например, мне не принципиально :) – MihailPw Jun 30 '17 at 13:44
  • 2
    @AGS17: Ну это как бы плохо: правильность вызова не видна на этапе компиляции. Хуже того, оптимизатор имеет обычно право убрать ненужную временную переменную, как ему это запретить? – VladD Jun 30 '17 at 13:46
  • 2
    @AGS17: Само понятие «переменная, которую передали в качестве аргумента», выглядит сомнительно на мой вкус. Аргументом функции является выражение, не переменная. – VladD Jun 30 '17 at 13:47
  • @VladD благодарю за разжевывание, необходимую информацию я для себя подчеркнул :) – MihailPw Jun 30 '17 at 14:01
  • @AGS17: Сам по себе вопрос как раз очень хороший, не знаю, за что стоят минусы. – VladD Jun 30 '17 at 14:05
  • @VladD получение имени передаваемой переменной "для удобства разработки" выглядит как отладка через printf, за что и ставят минусы – user270576 Jun 30 '17 at 18:52
  • @user270576: В этом ответе вполне валидный юзкейс. Плюс вопрос по сути о теоретической возможности, то есть, об основах устройства CLR. – VladD Jun 30 '17 at 18:54
  • 1
    @VladD "Если вам имя переменной требуется не безусловно, а только в особых случаях с отладочными целями" - вполне валидный юзкейс? Debug brakepoint больше не работает? "Printf" в вызывающей функции будет иметь больше смысла и проще реализован. Формулировка вопроса заставляет думать, что речь идет не о теоретической возможности, а о костылях – user270576 Jun 30 '17 at 19:23
  • 1
    @user270576: Разумеется, валидный. Например, если это числомолотилка, то conditional breakpoint очень сильно замедляет пробег программы, так что вот такой вот код — вполне себе хороший костыль. Да, всё это можно сделать и прямее, за счёт большого ручной работы по логированию, но зачем? Кроме того: само знание фактажа о том, что имя поля или там название процедуры можно выцепить через рефлексию, а временной переменной — нет (или да, в некоторых случаях), является ни в коем случае не тривиальным. – VladD Jun 30 '17 at 20:53

3 Answers3

11

Например, вот так:

static void Main(string[] args)
{
    var world = "Hello, {0}!";
    DoAction(() => world);
}

static void DoAction(Expression<Func<string>> value)
{
    var me = (MemberExpression)value.Body;
    var variableName = me.Member.Name;
    var variableValue = value.Compile()();

    Console.WriteLine(variableValue, variableName);
}

Но это будет работать медленно. И точно так же будет медленно работать любая другая подобная "технология". Просто потому что уговорить компилятор сохранить имя переменной можно только превратив ее в поле или свойство - а способа быстро достать из объекта значение поля с неизвестным именем нет.

Pavel Mayorov
  • 58,537
  • К сожалению, этот вариант чуть нарушает красоту моего класса. В моем случае логичнее уже напрямую nameof() передавать :) Благодарю за ответ. – MihailPw Jun 30 '17 at 13:24
  • +1, но вот мнение разработчиков языка: https://stackoverflow.com/q/1718037/#comment1596566_1718037 – VladD Jun 30 '17 at 13:52
9

Если вам имя переменной требуется не безусловно, а только в особых случаях с отладочными целями - то при наличии отладочных символов его можно попытаться достать из исходника.

Например, как-то вот так:

static void Main(string[] args)
{
    string foobarbaz = null;
    Assert(foobarbaz != null);
    Assert(false);
    Assert(true && (false || new[] { true, false }[1]));
    Assert(false /* || true*/);
    Assert(
        false // || true
    );
    Assert /*trap*/ (false);
    Assert($"{{" == null);
}

[Conditional("DEBUG"), MethodImpl(MethodImplOptions.NoInlining)]
static void Assert(bool expression)
{
    if (expression) return;

    string expression_raw;
    var frame = new StackTrace(fNeedFileInfo: true).GetFrame(1);
    try
    {
        using (var file = File.OpenText(frame.GetFileName()))
        {
            for (int k = frame.GetFileLineNumber() - 1; k > 0; k--) file.ReadLine();
            for (int k = frame.GetFileColumnNumber() - 1; k > 0; k--) file.Read();

            var parser = new Parser(file);
            parser.Ensure(nameof(Assert));
            parser.ReadUntil('(');
            parser.Ensure("(");
            parser.BeginRecord();
            parser.ReadUntil(')');
            expression_raw = parser.EndRecord();
        }
    }
    catch (IOException)
    {
        expression_raw = "";
    }

    Console.WriteLine($"Assertion {expression_raw.Trim()} failed at {frame.ToString()}");
}

private class Parser
{
    private readonly TextReader input;
    private StringBuilder output;
    private char current, prev;

    public Parser(TextReader input)
    {
        this.input = input;
    }

    public void BeginRecord()
    {
        output = new StringBuilder();
    }
    public string EndRecord()
    {
        var result = output.ToString();
        output = null;
        return result;
    }

    private char Read()
    {
        var c = input.Read();
        if (c == -1) throw new EndOfStreamException();

        prev = current;
        current = (char)c;

        output?.Append(char.IsWhiteSpace(current) ? ' ' : current);
        return current;
    }

    public void Ensure(string v)
    {
        foreach (var c in v)
        {
            if (Read() != c) throw new IOException();
        }
    }

    public void ReadUntil(char end)
    {
        while (input.Peek() != end)
        {
            switch (Read())
            {
                case '(': ReadUntil(')'); Read(); break;
                case '[': ReadUntil(']'); Read(); break;
                case '{': ReadUntil('}'); Read(); break;
                case '\'': ReadCharacter(); if (Read() != '\'') throw new IOException(); break;
                case '"': ReadString(formattable: prev == '$'); Read(); break;

                case '/':
                    if (prev == '/')
                    {
                        do { Read(); } while (current != '\n' && current != '\r');
                    }
                    break;

                case '*':
                    if (prev == '/')
                    {
                        current = '\0';
                        do { Read(); } while (prev != '*' || current != '/');
                        current = '\0';
                    }
                    break;
            }
        }
    }

    private void ReadCharacter()
    {
        if (Read() == '\\') Read();
    }

    private void ReadString(bool formattable)
    {
        while (input.Peek() != '"')
        {
            if (formattable && input.Peek() == '{')
            {
                Read();
                if (input.Peek() != '{')
                {
                    ReadUntil('}');
                }
                Read();
                continue;
            }

            ReadCharacter();
        }
    }
}
Pavel Mayorov
  • 58,537
6

В C# 8.0 завезут новый атрибут:

CallerArgumentExpression

С его помощью можно будет получить имя переменной, которое было передано.

Цитирую ответ, который мне дали тут:

Если метод объявлен так:

public static class Debug
{
     public static void Assert(bool condition, [CallerArgumentExpression("condition")] string message = null);
}

и вызывается так:

Debug.Assert(someBoolean);
Debug.Assert(array != null);
Debug.Assert(array.Length == 1);

, то компилятор подставит значение второго аргумента:

Debug.Assert(someBoolean, "someBoolean");
Debug.Assert(array != null, "array != null");
Debug.Assert(array.Length == 1, "array.Length == 1");

Соответственно никаких просадок в производительности не будет, так как это будет делаться на этапе компиляции.

iluxa1810
  • 24,899