Есть ли в c# свойство, которое обязывает вызывать метод предка при его переопределении?

А вот кто знает, есть ли в c# (Unity3D) свойство, которое обязывает вызывать метод предка при его переопределении?

Метод, который хочется отслеживать - OnDestroy. Вызывается при уничтожении объекта. Соответственно нужен в обоих скриптах и разделить его никак не получится.

Смысл прост. Переопределили не посмотрев на предка, слетел функционал. Большой вопрос в том как это предотвратить? (административные методы не помогут)

Может в райдере настроить линтер как-то можно? Например добавить свойство и некую реакцию на него?

class parent
{
 
  protected  virtual void OnDestroy()
  {
     // some happens
  }
}
 
class child : parent
{
  protected  override void OnDestroy()
  {
     // Ошибка, потому как нет вызова base.OnDestroy();
 
  }
 
}

Ответы (2 шт):

Автор решения: Артём Оконечников

Гарантировать этого вы не можете. Это противоречит самой идеи виртуальных методов. Но, вы можете изменить шаблон таким образом, что бы вызов определенного метода не зависел от качества памяти программиста:

abstract class Parent
{
    public void foo()
    {
        bar();
        M1();
    }

    protected abstract void M1();

    private void bar()
    {
        // Важный базовый функционал
    }
}

class Child : Parent
{
    protected override void M1()
    {
        // ...
    }
} 

Теперь, описываемое вами поведение имплементировано в методе foo.


Хорошо, если вопрос ставить следующим образом: можно без изменения дизайна как-то гарантировать вызов базового класса, то ответ — нет (если это не конструктор, конечно). Да и как бы это работало? Цепочка наследования может быть гораздо глубже чем Base → Derived и как бы регулировался этот вызов? Единственная, насколько мне известная, возможность осуществить невиртуальный вызов это base.Method()

Остальные возможности язык оставляет вам и они связаны с изменением дизайна. От шаблонного класса до цепочки обязанностей.


И последнее, если кто-то проектирует базовый класс в котором есть важный-важный метод onDestroy вызов которого обязателен (по вашим словам) и делает этот метод виртуальным! то, может, проблема лежит в некоторой другой плоскости? :-)

→ Ссылка
Автор решения: VladD

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

Итак, давайте вооружимся вот этими полезными статьями:

а также можно посмотреть в этот немного устаревший ответ:

Установим нужные пакеты в Visual Studio и создадим проект типа Analyzer with Code Fix. Никакого хорошего Code Fix придумать нельзя: как именно вызывать метод базового класса, должен осознанно решить сам программист, поэтому у нас Code Fix не будет.

У нас получатся 5 проектов в Visual Studio, из которых проект с code fix я удалил (и сделал другие очевидные изменения в source.extension.vsixmanifest и CallBaseAnalyzer.Package.csproj, чтобы всё компилировалось):

Solution Explorer

Самое интересное — код анализатора — лежит в первом проекте. Его код заменяем на вот такой: (пояснения к коду даны по ходу в комментариях)

using System.Collections.Immutable;
using System.Linq;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace CallBaseAnalyzer
{
    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    public class CallBaseAnalyzerAnalyzer : DiagnosticAnalyzer
    {
        public const string DiagnosticId = "CallBaseAnalyzer";

        private static readonly string Title = "Base virtual function not called";
        private static readonly string MessageFormat =
            "The override of function {0} must call base";
        private static readonly string Description =
            "The virtual function override must call the base function";
        private const string Category = "Reliability";

        // соберём описание нашего правила
        private static readonly DiagnosticDescriptor Rule =
            new DiagnosticDescriptor(
                DiagnosticId, Title, MessageFormat, Category,
                DiagnosticSeverity.Error, // укажен, что это ошибка, а не предупреждение
                isEnabledByDefault: true, description: Description);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
            ImmutableArray.Create(Rule);

        public override void Initialize(AnalysisContext context)
        {
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
            context.EnableConcurrentExecution();

            // будет вызвано для каждого метода
            context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Method);
        }

        private static void AnalyzeSymbol(SymbolAnalysisContext context)
        {
            // получаем символ
            var methodSymbol = (IMethodSymbol)context.Symbol;

            // если это не переопределение, нам не интересно
            if (!methodSymbol.IsOverride)
                return;

            // получаем тот метод, который переопределён
            // (он должен быть вызван в теле метода, мы будем это проверять)
            var overriddenMethod = methodSymbol.OverriddenMethod;

            // спускаемся до синтаксического дерева, получаем тело метода
            var declaringSyntaxes = methodSymbol.DeclaringSyntaxReferences;
            // https://stackoverflow.com/a/38294041/276994
            // обыно дожно быть одно, для partial-методов два,
            // если метод из метаданных (ref. assembly), то ноль
            if (declaringSyntaxes.Count() == 0)
                return;

            bool foundBaseCall = false;

            foreach (var declaringSyntax in declaringSyntaxes)
            {
                var methodTopSyntaxNode = declaringSyntax.GetSyntax();
                // ищем все ссылки на базовый класс (base)
                var baseMentionSyntaxes =
                        methodTopSyntaxNode.DescendantNodes()
                                           .OfType<BaseExpressionSyntax>();
                foreach (var baseMentionSyntax in baseMentionSyntaxes)
                {
                    // найдём включающий его вызов метода поиском вверх до вершины метода
                    InvocationExpressionSyntax invocationNode = null;
                    for (SyntaxNode curr = baseMentionSyntax.Parent;
                         curr != methodTopSyntaxNode;
                         curr = curr.Parent)
                    {
                        if (curr is InvocationExpressionSyntax ies)
                        {
                            invocationNode = ies;
                            break;
                        }
                    }

                    if (invocationNode == null) // не нашли? к следующему base
                        continue;

                    // даже если нашли какой-то вызов, надо ещё проверить,
                    // а тот ли это вызов, что нам нужно
                    // получаем символ у семантической модели
                    // https://stackoverflow.com/a/30721617/276994
                    var semanticModel =
                        context.Compilation.GetSemanticModel(invocationNode.SyntaxTree);
                    var symInfo = semanticModel.GetSymbolInfo(invocationNode);
                    // берём метод, который вызывается данным вызовом
                    var innerMethodSymbol = (IMethodSymbol)symInfo.Symbol;
                    // если его нет (например, у нас некомпилирующийся код), отваливаем
                    if (innerMethodSymbol == null)
                        return;

                    // сравниваем с тем, который мы ищем
                    if (SymbolEqualityComparer.Default.Equals(innerMethodSymbol,
                                                              overriddenMethod))
                    {
                        foundBaseCall = true; // окей, вызов именно базового метода
                        break; // выходим, дальше искать нечего
                    }
                }

                if (foundBaseCall)
                    break;  // выходим, дальше искать нечего
            }

            // если результаты поисков отрицательные...
            if (!foundBaseCall)
            {
                // выдаём диагностическое сообщение
                var diagnostic = Diagnostic.Create(Rule,
                                                   methodSymbol.Locations[0],
                                                   methodSymbol.Name);
                context.ReportDiagnostic(diagnostic);
            }
        }
    }
}

Не забываем про unit-тесты, благо проект с ними любезно создал для нас wizard:

[TestClass]
public class CallBaseAnalyzerUnitTest
{
    [TestMethod]
    public async Task OverrideIsCalled()
    {
        var test = @"
namespace Test
{
    class BaseClass
    {
        protected virtual int GetValue() => 1;
    }

    class DerivedClass : BaseClass
    {
        protected override int GetValue()
        {
            if (1 > 0) return 5; else return base.GetValue();
        }
    }
}";

        await VerifyCS.VerifyAnalyzerAsync(test);
    }

    [TestMethod]
    public async Task OverrideIsNotCalled()
    {
        var test = @"
namespace Test
{
    class BaseClass
    {
        protected virtual int GetValue() => 1;
    }

    class DerivedClass : BaseClass
    {
        protected override int {|#0:GetValue|}()
        {
            if (1 > 0) return 5; else return 6;
        }
    }
}";

        var expected = VerifyCS.Diagnostic("CallBaseAnalyzer")
                               .WithLocation(0)
                               .WithArguments("GetValue");
        await VerifyCS.VerifyAnalyzerAsync(test, expected);
    }
}

Запустим vsix-проект на выполнение, получим экспериментальный экземпляр Visual Studio, в котором применяется наш анализатор:

Запись работы Студии


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

Возможностей для этого есть много. Самый простой вариант — захардкодировать проверяемую функцию OnDestroy. Например, так. В класс-анализатор добавляем фильтрующий метод, который ограничивает проверки переопределениями метода OnDestroy класса TestOuter.TestInner.BaseClass:

private static bool FilterMethod(IMethodSymbol methodSymbol)
{
    if (methodSymbol.Name != "OnDestroy")
        return false;

    // ищем самый дальний переопределённый метод
    var topmostOverriddenSymbol = methodSymbol;
    while (topmostOverriddenSymbol.OverriddenMethod != null)
        topmostOverriddenSymbol = topmostOverriddenSymbol.OverriddenMethod;

    // проверяем, что этот метод:
    return topmostOverriddenSymbol is
    {
        ContainingType: // содержится прямо в типе...
        {
            Name: "BaseClass", // по имени BaseClass,
            ContainingSymbol:  // который содержится непосредственно в
            {
                Kind: SymbolKind.Namespace, // пространстве имён
                Name: "TestInner",          // TestInner
                ContainingSymbol:           // которое, в свою очередь, содержится в
                {
                    Kind: SymbolKind.Namespace, // пространстве имён
                    Name: "TestOuter",          // TestOuter
                    ContainingSymbol:           // которое, в свою очередь, содержится в
                    {
                        Kind: SymbolKind.Namespace,  // пространстве имён
                        CanBeReferencedByName: false // без имени (то есть, корневом)
                    }
                }
            }
        }
    };
}

В методе AnalyzeSymbol дописываем применение фильтра:

private static void AnalyzeSymbol(SymbolAnalysisContext context)
{
    // получаем символ
    var methodSymbol = (IMethodSymbol)context.Symbol;

    // если это не переопределение, нам не интересно
    if (!methodSymbol.IsOverride)
        return;

    if (!FilterMethod(methodSymbol))
        return;

    ...

Можно придумать более изящный путь, например, указывать нужные базовые методы при помощи атрибута, при этом фильтр тоже должен быть обновлён.

Вам понадобится подключить анализатор к проекту.

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

→ Ссылка