C#. Сортировка членов типа с помощью ReSharper


Существуют некоторые соглашения касаемые структуры класса, и того, в каком порядке должны располагаться его члены.
Вот, например, правила которые использует StyleCop, возможно, в вашей компании есть свои собственные.
Поддерживать структуру вручную довольно тяжело, скучно и отнимает много времени, особенно когда в классе довольно большое количество свойств, полей, методов и.т.д.
В этом посте речь пойдет о том, как с помощью ReSharper автоматизировать этот процесс.

Проблема


Иногда при быстром кодинге, мы забываем о сортировке членов и получается что-то вроде этого.
Каша
    public class DemoClass : IEquatable<DemoClass>
    {
        public string SecondProperty { get; set; }

        private bool fieldBool;

        public override int GetHashCode()
        {
            return base.GetHashCode();
        }

        public string SomeProperty { get; set; }

        public void DoSomething()
        {
        }

        private string fieldString;

        public DemoClass(bool fieldBool, string fieldString)
        {
            this.fieldBool = fieldBool;
            this.fieldString = fieldString;
        }

        public static void DoSomethingStatic()
        {
        }

        public bool Equals(DemoClass other)
        {
            throw new NotImplementedException();
        }
    }


Сортировка по умолчанию


Но, к счастью, в ReSharper есть инструмент Code Cleanup. И если запустить его со включенной опцией Reorder Type Members, мы получим класс с упорядоченными членами.
После
    public class DemoClass : IEquatable<DemoClass>
    {
        private bool fieldBool;
        private string fieldString;

        public DemoClass(bool fieldBool, string fieldString)
        {
            this.fieldBool = fieldBool;
            this.fieldString = fieldString;
        }

        public string SomeProperty { get; set; }

        public string SecondProperty { get; set; }

        public bool Equals(DemoClass other)
        {
            throw new NotImplementedException();
        }

        public override int GetHashCode()
        {
            return base.GetHashCode();
        }

        public void DoSomething()
        {
        }

        public static void DoSomethingStatic()
        {
        }
    }


Настраиваемые правила


Но что если сортировка по умолчанию нас не устраивает, и отличается от той, которая принята у вас в качестве стандарта?
В этом случае ReSharper позволяет переопределить правила используемые по умолчанию.
Идем в ReSharper -> Options -> Code Editing -> C# -> Type Member Layout и включаем Custom Layout.
Перед нами откроется XML который описывает порядок сортировки.
Пример
<Patterns xmlns="urn:shemas-jetbrains-com:member-reordering-patterns">
  <Pattern>
    <!--static fields and constants-->
    <Entry>
      <Match>
        <Or Weight="100">
          <Kind Is="constant"/>
          <And>
            <Kind Is="field"/>
            <Static/>
          </And>
        </Or>
      </Match>
      <Sort>
        <Kind Order="constant field"/>
      </Sort>
    </Entry>
    <Entry>
      ...
    </Entry>
  </Pattern>
  <Pattern>
    ...
  <Pattern>
</Patterns>


Элемент Patterns родительский элемент, может содержать внутри себя множество элементов Pattern, который, в свою очередь, может содержать множество элементов Entry.
Элемент Entry представляет собой запись, с которой мы будем осуществлять какие-то действия. Порядок Entry в элементе Pattern влияет на порядок элементов.
Предположим что нам нужен следующий порядок: конструкторы, методы, остальные члены.
Для этого мы можем написать такое правило:
Пример
<?xml version="1.0" encoding="utf-8"?>

<Patterns xmlns="urn:shemas-jetbrains-com:member-reordering-patterns">
  <Pattern>
    <Entry>
      <Match>
        <Kind Is="constructor" />
      </Match>
    </Entry>
    <Entry>
      <Match>
        <Kind Is="method" />
      </Match>
    </Entry>
    <Entry />
  </Pattern>
</Patterns>


Каждый элемент Entry содержит вложенный элемент Match, в котором мы описываем, что ищем. Мы можем задать более подробные правила для поиска и пользоваться логическими операторами Or, And, Not и операндами:
Операнд Значения
<Kind Is="..."/> Атрибут Is может быть class, struct, interface, enum, delegate, type, constructor, destructor, property, indexer, method, operator, field, constant, event, member
<Name Is="..." [IgnoreCase=«true/false»] /> Атрибут Is может содержать регулярное выражение
<HasAttribute CLRName="..." [Inherit=«true/false»] /> Атрибут CLRName может содержать регулярное выражение
<Access Is="..."/> Атрибут Is может быть public, protected, internal, protected-internal, private
<Static/>
<Abstract/>
<Virtual/>
<Override/>
<Sealed/>
<Readonly/>
<ImplementsInterface CLRName="..."/> Атрибут CLRName может содержать регулярное выражение
<HandlesEvent />

Так же, каждый элемент Entry может содержать Sort, что влияет на сортировку найденных членов в пределах одного Entry.
Например, нам надо найти статические поля и константы и расположить их в самом начале класса. А так же отсортировать их по имени. Для этого нам надо добавить следующую запись в начало паттерна.
Пример
    <Entry>
      <Match>
        <Or>
          <Kind Is="constant" />
          <And>
            <Kind Is="field" />
            <Static />
          </And>
        </Or>
      </Match>
      <Sort>
        <Name />
      </Sort>
    </Entry>


Сортировка может быть по нескольким значениям, следующий пример отсортирует все методы по уровню доступа и имени.
Пример
    <Entry>
      <Match>
          <Kind Is="method"/>
      </Match>
      <Sort>
        <Access Order="public internal protected-internal protected private" />
        <Name/>
      </Sort>
    </Entry>


Группировка


Так же поддерживается группировка.
Допустим, что методы надо разделять на статичные и экземплярные, сортировать по имени и оборачивать в соответствующие регионы. Для этого нам надо создать два различных элемента Entry с дочерним элементом Group:
Пример
    <Entry>
      <Match>
        <And>
          <Kind Is="method" />
          <Static/>
        </And>
      </Match>
      <Group Region="Static methods" />
    </Entry>
    <Entry>
      <Match>
        <Kind Is="method" />
      </Match>
      <Group Region="Instance methods" />
    </Entry>


Что если, все члены интерфейсов, которые реализовал класс, необходимо отсортировать по имени интерфейса и обернуть в регион с именем этого интерфейса? Для этого мы могли бы сделать так:
Пример
    <Entry>
      <Match>
        <Kind Is="member" />
        <ImplementsInterface/>
      </Match>
      <Sort>
        <ImplementsInterface/>
      </Sort>
      <Group>
        <ImplementsInterface Region="${ImplementsInterface} Members" />
      </Group>
    </Entry>


В этом примере, переменная ${ImplementsInterface} будет равна названию интерфейса.
Также, элемент Pattern поддерживает аттрибут RemoveAllRegions, который может быть установлен в true или false (по умолчанию false). Если он установлен в true, то при группировке, будут удалены все регионы которые были до нее.

Вес


Что если условию (Match) соотвествуют сразу несколько типов?
Предположим мы хотим сгруппировать все приватные члены в один регион, кроме методов. Мы могли бы написать следующее правило:
Пример
    <Entry>
      <Match>
        <Access Is="private" />
      </Match>
      <Group Region="Private members" />
    </Entry>
    <Entry>
      <Match>
        <Kind Is="method" />      
      </Match>
      <Group Region="All methods" />
    </Entry>


Но, если у нас есть приватный метод, то он попадает под оба правила. Первое правило применится раньше и под второе правило попадут все методы кроме приватных. В итоге, регион который будет содержать приватные члены, будет так же содержать и приватные методы.
Каждый успешно выполненный операнд дает 1 очко веса по умолчанию и эти очки суммируются. В данном случае, в каждом из элементов по одному операнду: <Access Is=«private» /> и <Kind Is=«method» /> соотвественно. Если мы хотим, чтобы второе правило выполнилось, мы можем использовать аттрибут Weight на операнде.
Пример
    <Entry>
      <Match>
        <Access Is="private" />
      </Match>
      <Group Region="Private members" />
    </Entry>
    <Entry>
      <Match>
        <Kind Is="method" Weight="5"/>      
      </Match>
      <Group Region="All methods" />
    </Entry>


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

Паттерны


В самом начале, я говорил об элементе Pattern, он так же может содержать элемент Match и этот элемент определяет где будут действовать правила которые содержит паттерн.
А теперь, представим ситуацю, когда во всех интерфейсах, нам необходима другая сортировка. В такой ситуации мы могли бы создать отдельный элемент Pattern
Пример
  <Pattern>
      <Match>
        <Kind Is="interface" />
      </Match>
      ...
  </Pattern>


Все правила, которые будут описаны в приведенном паттерне, будут влиять только на интерфейсы.

Заключение


В этом посте я хотел показать некоторые из возможностей ReSharper, которые, может быть, сделают вашу жизнь легче.
Этот пост не претендует на исчерпывающую документацию по данному вопросу.
Источники:

Комментарии 11

    +1
    Спасибо. Забираю в избранное.
      +3
      Извините за оффтоп. Никогда не понимал кодестайлы где приватные поля идут первыми. Публичные методы класса есть его интерфейс (логический) и когда он сверху то быстрее вникнуть в роль класса.
        +2
        StyleCop согласен с вами.
        В посте же, все сортировки показаны только для примера.
        0
        Честно говоря с навигацией по классу которую предоставляют MSVS + Resharper или Idea не так уж и сложно наоходить и свойства и методы.

        Но что-то мне подсказывает, что контекстно-связаные свойства находящиеся в пределах одного экрана несколько более читаемы, чем совершенно никак не связаные, но отсортированые члены класса (например add, close, remove).
          0
          Навигация, конечно, да, Go to Everything и Go to File Member очень помогает. Сортировка, скорее даже не для быстрого нахождения чего либо, а для поддержания хоть какого-то порядка.

          Дело вкуса и стандарта, которого вы придерживаетесь.
          Ведь для этого пост и был написан, чтобы показать что можно настроить сортировку, как вам угодно.
          Вы ведь можете сортировать, например, только по типу члена, не трогая имя.
          0
          Очень полезная вещь. Только нужно быть осторожным при Cleanup всего проекта, особенно с частями унаследованного кода, так как он может полагаться на определённое расположение полей в структурах. Для предосторожности, я бы порекомендовал отключить сортировку для структур вообще по умолчанию (а также автоматическое преобразование свойств в автосвойства, кстати). Один раз я очень удивился, когда после Cleanup программа перестала работать, хотя она и не содержала небезопасный код.
            +1
            По умолчанию, Resharper создает правило для сортировки, которое вы описали:
            Спойлер
              <!--Do not reorder COM interfaces and structs marked by StructLayout attribute-->
              <Pattern>
                <Match>
                  <Or Weight="100">
                    <And>
                      <Kind Is="interface"/>
                      <Or>
                        <HasAttribute CLRName="System.Runtime.InteropServices.InterfaceTypeAttribute"/>
                        <HasAttribute CLRName="System.Runtime.InteropServices.ComImport"/>
                      </Or>
                    </And>
                    <HasAttribute CLRName="System.Runtime.InteropServices.StructLayoutAttribute"/>
                  </Or>
                </Match>
              </Pattern>
            


            0
            Очень полезная штука, давно применяю. Вкупе с проверкой naming conventions позволяет не забивать голову стандартами того или другого проекта.
            Только у нас есть правило — не делать cleanup существующему коду. Это приводит к каше в code review и лучям зла от тех, кто их делает. Cleanup делаем только для новых файлов, где он не приведёт к «лишним» изменениям.
              0
              Что-то я не совсем уловил мысль. А если я смотрю в старый файл и вижу говнокод, который требует рефакторинга (переносов, переименования), то сделать его не могу?
                0
                Да, потому что если туда потом ещё внести изменения, то не понятно, что поменялось на ревью.
                  0
                  А может просто зарефакторить, переименовать, перенести и зачекинить под задачей РЕФАКТОРИНГ ИЛИ ЧТО ТАМ ЕЩЁ?

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

            Самое читаемое