Почему GetProperties дважды отображает защищенное свойство (объявленное в базовом классе)?

Когда я объявляю следующие простые классы:

class Class1<T>
{
    protected virtual T Prop1 { get; set; }
    protected virtual string Prop2 { get; set; }
}

class Class2 : Class1<string>
{
    protected override string Prop1 { get; set; }
    protected override string Prop2 { get; set; }
}

и теперь я использую Reflection для получения свойств Class2 следующим образом:

var hProperties = typeof(Class2).GetProperties(BindingFlags.NonPublic | BindingFlags.Instance);

тогда Prop2 будет указан один раз, пока Prop1 будет указан дважды! Такое поведение кажется мне странным. Не следует ли считать Prop1 и Prop2 одинаковыми?

Что я могу сделать, чтобы иметь Prop1 только один раз в hProperties? Я не хочу использовать BindingFlags.DeclaredOnly, так как я также хочу, чтобы другие защищенные свойства Class1 не были переопределены.

Ответ 1

Посмотрите на скомпилированные метаданные сборки, чтобы убедиться, что эти два свойства имеют идентичную структуру, кроме имени:

enter image description here

Я использую ILDASM вместо обычных инструментов декомпилятора, чтобы убедиться, что ничто не скрыто или не отображается более дружелюбно. Эти два свойства идентичны друг от друга.

Один из двух возвращаемых свойств Prop1 - от Class1, а один из них - от Class2.

Это кажется ошибкой. Кажется, что ошибка в том, что элементы базового класса неправильно добавлены к результатам. Если DeclaredOnly не указано то также должны быть возвращены все унаследованные свойства.

Я использую DotPeek и расширение Reflector VS, которое позволяет отлаживать декомпилированный код BCL для отладки кода отражения. Поведение, наблюдаемое в этом вопросе, запускается в этом методе:

    private void PopulateProperties(RuntimeType.RuntimeTypeCache.Filter filter, RuntimeType declaringType, Dictionary<string, List<RuntimePropertyInfo>> csPropertyInfos, bool[] usedSlots, ref RuntimeType.ListBuilder<RuntimePropertyInfo> list)
    {
      int token = RuntimeTypeHandle.GetToken(declaringType);
      if (MetadataToken.IsNullToken(token))
        return;
      MetadataEnumResult result;
      RuntimeTypeHandle.GetMetadataImport(declaringType).EnumProperties(token, out result);
      RuntimeModule module = RuntimeTypeHandle.GetModule(declaringType);
      int numVirtuals = RuntimeTypeHandle.GetNumVirtuals(declaringType);
      for (int index1 = 0; index1 < result.Length; ++index1)
      {
        int num = result[index1];
        if (filter.RequiresStringComparison())
        {
          if (ModuleHandle.ContainsPropertyMatchingHash(module, num, filter.GetHashToMatch()))
          {
            Utf8String name = declaringType.GetRuntimeModule().MetadataImport.GetName(num);
            if (!filter.Match(name))
              continue;
          }
          else
            continue;
        }
        bool isPrivate;
        RuntimePropertyInfo runtimePropertyInfo = new RuntimePropertyInfo(num, declaringType, this.m_runtimeTypeCache, out isPrivate);
        if (usedSlots != null)
        {
          if (!(declaringType != this.ReflectedType) || !isPrivate)
          {
            MethodInfo methodInfo = runtimePropertyInfo.GetGetMethod();
            if (methodInfo == (MethodInfo) null)
              methodInfo = runtimePropertyInfo.GetSetMethod();
            if (methodInfo != (MethodInfo) null)
            {
              int slot = RuntimeMethodHandle.GetSlot((IRuntimeMethodInfo) methodInfo);
              if (slot < numVirtuals)
              {
                if (!usedSlots[slot])
                  usedSlots[slot] = true;
                else
                  continue;
              }
            }
            if (csPropertyInfos != null)
            {
              string name = runtimePropertyInfo.Name;
              List<RuntimePropertyInfo> list1 = csPropertyInfos.GetValueOrDefault(name);
              if (list1 == null)
              {
                list1 = new List<RuntimePropertyInfo>(1);
                csPropertyInfos[name] = list1;
              }
              for (int index2 = 0; index2 < list1.Count; ++index2)
              {
                if (runtimePropertyInfo.EqualsSig(list1[index2]))
                {
                  list1 = (List<RuntimePropertyInfo>) null;
                  break;
                }
              }
              if (list1 != null)
                list1.Add(runtimePropertyInfo);
              else
                continue;
            }
            else
            {
              bool flag = false;
              for (int index2 = 0; index2 < list.Count; ++index2)
              {
                if (runtimePropertyInfo.EqualsSig(list[index2]))
                {
                  flag = true;
                  break;
                }
              }
              if (flag)
                continue;
            }
          }
          else
            continue;
        }
        list.Add(runtimePropertyInfo);
      }
    }

Почему поведение исчезает для общедоступных свойств?

      if (!(declaringType != this.ReflectedType) || !isPrivate)

Там проверка на это.

Class1<string>.Prop2 отфильтровывается здесь:

              bool flag = false;
              for (int index2 = 0; index2 < list.Count; ++index2)
              {
                if (runtimePropertyInfo.EqualsSig(list[index2]))
                {
                  flag = true;
                  break;
                }
              }
              if (flag)
                continue;

потому что EqualsSig возвращает true. Похоже, что свойства дедуплицируются по имени и по sig, если вы запрашиваете частных членов... Я не знаю, почему. Кажется преднамеренным, однако.

Утомляет следовать этому свернутому коду. Это лучше и прокомментировано. Я подозреваю, что они удаляют частные свойства, потому что вы можете повысить привилегии, наследуя от какого-то класса, чтобы получить все частные члены.

И вот ответ:

// For backward compatibility, even if the vtable slots don't match, we will still treat
// a property as duplicate if the names and signatures match.

Таким образом, они добавили хак для обратной совместимости.

Вам нужно будет добавить свою собственную обработку, чтобы получить нужное поведение. Возможно, Fastreflect может помочь.

Ответ 2

Это потому, что Class1<T>.Prop1 и Class2.Prop1 не являются тем же свойством... в Class1<T>, свойство имеет тип T, тогда как в Class2 это тип string.

Class2 - это всего лишь одна реализация общего класса Class1<T>, но могут быть и другие... рассмотрим это:

class Class3 : Class1<int>
{
    protected override int Prop1 { get; set; }
    protected override string Prop2 { get; set; }
}

Если вы считаете, что Class2.Prop1 является тем же свойством, что и Class1<T>.Prop1, то Class3.Prop1 является тем же свойством, что и Class1<T>.Prop1, из этого следует, что Class2.Prop1 также совпадает с Class3.Prop1, что ясно не имеет смысла: один имеет тип string, другой тип int...