Pull to refresh

ReSharper: Value Tracking

Reading time 6 min
Views 902
Я уже писал о новой фиче 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. Код со вложенными лямбдами сильно затруден для понимания обычным человеком, что делает анализ еще более востребованным. Более того, можете попытаться создать коллекцию вложенных лямбд — и это будет работать! Но такие упражнения я оставлю читателю и нелегкой реальной жизни.
Tags:
Hubs:
+11
Comments 12
Comments Comments 12

Articles