ReSharper: Value Tracking

    Я уже писал о новой фиче 5-го Решарпера Call Hierarchy. Логичным развитием Call Hierarchy является Value Tracking. Value Tracking создан для того, чтобы помочь разработчику понять, как в конкретную точку программы могли придти неверные данные или куда эти данные могли уйти. Как следствие, становится легче расследовать причины NullReferenceException или неправильное поведение и вывод.

    Я опять же не буду глубоко теоретизировать, а обзорно покажу как и в каких сценариях работает Value Tracking.

    Простой пример


    Рассмотрим простой пример. В методе VisitData происходит NullReferenceException, давайте выясним, откуда прихожит null. Поместите каретку на использование параметра dc в методе VisitData и запустите анализ (R# -> Inspect -> Value Origin):

     public class Main
     {
      void Start()
      {
       Console.WriteLine(Do(new DataClass()));
      }

      void Start2()
      {
       Console.WriteLine(Do(null));
      }

      int Do(DataClass data)
      {
       var v = new Visitor();
       return v.VisitData(data);
      }
     }

     public class DataClass
     {
      public int GetData()
      {
       return 0;
      }
     }

     public class Visitor
     {
      public int VisitData(DataClass dc)
      {
       return dc.GetData();
      }
     }


    * This source code was highlighted with Source Code Highlighter.


    image

    Если вы самостоятельно запустите анализ на приведенном примере и будите навигироваться по дереву результатов, то обнаружите, что дерево содержит все интересные для программиста узлы:
    1. Непосредственно использование dc (как раз то место, где происходит исключение)
    2. Передача параметра data в метода VisitData
    3. Вызов метода Do с «хорошими данными» (в дереве интересующие нас данные подсвечены болдом)
    4. Вызов метода Do с null – искомая проблема
    По-большому счету, ничего кроме массовых Find Usages сделано не было. Но ValueTracking:
    • Пропускает несущественные шаги, что экономит время
    • Показывает данные в удобном виде, т.е. позволяет быстро, не теряя концентрации на проблеме и не отслеживая глазами все использования символов, найти источник проблемы.
    Value Tracking особенно удобен, если имена переменных постоянно меняются, данные складываются в коллекции, передаются через замыкания. Давайте перейдем к рассмотрению этих более сложных и интересных случаев.

    Наследование


    На этот раз у нас есть интерфейс, его реализации, поля, инициализаторы полей, конструкторы. Попытаемся выяснить, какие значения может выводить на экран метод Main.Start. Для этого выделим выражение dataProvider.Foo и вызовем на нем Value Origin:

    public interface IInterface
     {
      int Foo();
     }

     public class Base1 : IInterface
     {
      public virtual int Foo()
      {
       return 1;
      }
     }

     public class Base2 : IInterface
     {
      private readonly int _foo = 2;

      public Base2()
      {  
      }

      public Base2(int foo)
      {
       this._foo = foo;
      }

      public virtual int Foo()
      {
       return _foo;
      }
     }

     public class Main
     {
      public void Start(IInterface dataProvider)
      {
       Console.WriteLine(dataProvider.Foo());
      }

      public void Usage()
      {
       Start(new Base2(3));
      }
     }


    * This source code was highlighted with Source Code Highlighter.


    image

    В результатах Value Tracking мы видим:
    1. Реализацию метода Foo, которая возвращает константу 1
    2. Реализацию метода Foo, которая возвращает значение поля _foo, а также все источники значений для этого поля:
      1. Присвоение значения этому полю в конструкторе
      2. Вызов конструктора с параметром 3
      3. Инициализатор этого поля со значением 2
    Т.е. мы буквально за несколько шагов нашли все возможнные значения. Представьте теперь, сколько времени вы сэкономите, если у вас развесистые иерархии и сложная логика?

    Коллекции


    Теперь рассмотрим работу с коллекциями. Попробуем выяснить, множество всех значений, которые будут выведено на экран следующим кодом. Для этого встанем на использование i внутри Console.WriteLine и запустим анализ Value Origin:

    class Main
    {
     void Foo()
     {
      var list = new List<int>();
      list.AddRange(GetData());
      list.AddRange(GetDataLazy());
      ModifyData(list);

      foreach (var i in list)
      {
       Console.WriteLine(i);
      }
     }

     void ModifyData(List<int> list)
     {
      list.Add(6);
     }

     private IEnumerable<int> GetData()
     {
      return new[] { 1, 2, 3 };
     }

     IEnumerable<int> GetDataLazy()
     {
      yield return 4;
      yield return 5;
     }
    }


    * This source code was highlighted with Source Code Highlighter.


    image

    Мы нашли и явное создание массива, и значения, которые приходят из ленивого энумератора, и даже вызов метода Add. Великолепно!

    Коллекции в обратную сторону, или куда уходят значения


    А теперь попробуем в обратную сторону, посмотрим, куда попадет число 5. Выделяем его и вызываем Value Destination:

    public class testMy
    {
     void Do()
     {
      int x = 5;
      var list = Foo(x);

      foreach (var item in list)
      {
       Console.WriteLine(item);
       }
     }

     List<int> Foo(int i)
     {
      var list = new List<int>();
      list.Add(i);
      return list;
     }
    }


    * This source code was highlighted with Source Code Highlighter.


    image

    Достаточно быстро мы выяснили, что число 5:
    1. Передано в метод Foo
    2. Добавляется в коллекцию
    3. Коллекция возвращается и используется
    4. Элементы коллекции выводятся на экран

    В этом и предыдущем примерах обратите внимание, что как только Value Tracking переходит от отслеживания значения к отслеживанию коллекции, то соответствующие узлы в дереве помечаются специальным розовым значком.

    Лямбды


    Лямбды, с ними, особенно если их много и не дай бой они вложенные, постоянно возникают проблемы. Давайте посмотрим, как R# работает в следующей ситуации. При этом теперь попробуем прослеживать пусть значений в обе стороны:

    public class MyClass
    {
     void Main()
     {
      var checkFunction = GetCheckFunction();
      Console.WriteLine(checkFunction(1));
     }

     Func<int, bool> GetCheckFunction()
     {
      Func<int, bool> myLambda = x =>
                  {
                   Console.WriteLine(x);
                   return x > 100; //искать будем отсюда
                  };
      return myLambda;
     }
    }


    * This source code was highlighted with Source Code Highlighter.

    Сначала будем искать откуда берутся значения параметра x. Выделяем его использование в вызове Console.WriteLine и вызываем Value Origin:

    image

    1. Найдена содержащая параметр лямбда
    2. Далее анализ отследил, куда эта лямбда передается. Обратите внимание, что все узлы, в которых мы отслеживаем лямбду, помечены специальным значком
    3. На последнем шаге мы видим, что лямбда вызывается с аргументом 1, это и есть искомое значение для x

    Попробуем теперь найти, где используется значение возвращаемое лямбдой. Выделяем x>100, и вызываем Value Destination (R# -> Inspect -> Value Destination):

    image
    1. Анализ отслеживает, что выраженеи возвращается как результат выполнения лямбды
    2. Далее R# отследил, куда лямбда передавалась
    3. В конце мы видим вызов метода WriteLine, который и использует возвращаемое лямбдой значение

    Более сложный пример со вложенными ламбдами вы можете легко изготовить сами, заменив вывод на экран (Console.WriteLine) двумя строчками:

    Func<Func<int, bool>, int, bool> invocator = (func, i) => func(i);
    Console.WriteLine(invocator (checkFunction,1));


    * This source code was highlighted with Source Code Highlighter.

    Анализ по-прежнему будет работать и вы легко выясните, куда попадает значение выражения x>100. Код со вложенными лямбдами сильно затруден для понимания обычным человеком, что делает анализ еще более востребованным. Более того, можете попытаться создать коллекцию вложенных лямбд — и это будет работать! Но такие упражнения я оставлю читателю и нелегкой реальной жизни.
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 12
    • +5
      Да, утилита написана с умом. Помню года 3 или 4 назад впервые её увидел.

      Что-то последнее время у меня появляется ощущение, что через пару лет выглядеть всё будет так:
      «Visual Studio — Плагин для ReSharper, позволяет компилировать код.»
    • 0
      Убрали бы тормоза они наконец-то, цены бы не было.
      • +1
        просто нужен мощный комп, где много много памяти )
      • 0
        походу они хотят чтоб с каждый новой версией VS и R# программисты проапгейдели системник.
        • +1
          ReSharper навигировал навигировал, да не вынавигировал :)
          • 0
            Только вот работать оно будет вовсе не всегда. Вот только что пробовал детектить поле, которое инжектит DI-Container — сказало что поле не присваивается нигде.
            • 0
              пример покажите
              • 0
                using Ninject.Core;

                interface IA
                {
                }

                class A : IA
                {
                }

                class B
                {
                  [Inject]
                  public IA A;
                }

                class Module : StandardModule
                {
                  public override void Load()
                  {
                    Bind<IA>().To<A>();
                  }
                }

                class Program
                {
                  static void Main()
                  {
                    IKernel kernel = new StandardKernel(new Module());
                    B b = kernel.Get<B>();
                    Console.WriteLine(b.A);
                    Console.ReadKey();
                  }
                }


                * This source code was highlighted with Source Code Highlighter.

                Примерно вот-так.
                • +1
                  p.s. Понимаю, что так не совсем чесно, и что присвоение на самом деле происходит где-то глубоко в Ninject, исходников какого у Решарпера нет, но все-же :)
                  • 0
                    Будет в 6.0 или 5.5 работать Find Usages на скомпилированном коде, будут и присвоения внутри библиотек показываться. Но в любом случае, решарпер никогда не сможет отслеживать вызовы через Reflection, просто потому, что это динамическое связывание и для его обработки надо запускать программу :)

                    Опять же, вас обычно уровень, на котором вы инжектиреуете не очень интересует. Интересно как ходят данные в сложных алгоритмах, а то что вы добрались до kernel.Get — уже достаточно. Далее вы берете в руки другую тулзу и разбираете ужаснейшые конфиги, или аттриюуты в вашем случае и со временем приходите к выводу, что Ninject — зло :)))
                    • 0
                      Ну задача состояла в том, чтобы что-то поломать :) Вроде удалось)
                      Относительно того, что Ninject — зло… это как у Черчилля — «демократия — худшая форма правления, если не считать всех остальных»

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.