Saturday, February 26, 2011

Подклассы или типы

В пятницу на очередном занятии возник вопрос - когда лучше делать подклассы, а когда можно обойтись просто свойством (например, сделать тип сущности)? Я так думаю, что если отличий в разных типах не много, то мутить с подклассами не стоит.

Например, если кирпичи в игре Арканоид отличаются только цветом и числом попаданий до уничтожения, то вполне можно обойтись либо типом и тогда возвращать соответсвующие свойства в зависимости от типа:

public Color Foreground()
{
   swith (type)
   {
     case StoneType.One: return Colors.Red;
   ............
}

Либо вообще вынести эти параметры в конструктор и типы не заводить:

public Stone(Color foreground, int countStrike)
{
  .....
}

А вот если отличий будет очень много, то класс будет замусорен кучей if или switch. Вот тогда имеет смысл делать подклассы и разносить логику и код. Разумеется, если какой-то из "типов" имеет дополнительные свойства, то это однозначно нужно выностить в подкласс.

Monday, February 21, 2011

Еще пример про делегаты

Задача такая: по имени функции (математической) получить результат ее вычисления. Самый простой вариант такой:
public class CallFunc1
{
    /// <summary>
    /// Вызываем функцию по имени
    /// </summary>
    public double GetFuncValue(string funcName, double arg)
    {
        switch (funcName.ToUpper())
        {
            case "SIN": return Math.Sin(arg);
            case "COS": return Math.Cos(arg);
            case "TAN": return Math.Tan(arg);
            case "SQRT": return Math.Sqrt(arg);
        }

        throw new ArgumentException("Такой функции нет в списке зарегистрированных функций", funcName);
    }
}

Но тут мы вспоминаем, что квадратный корень нельзя вычислять от отрицательного значения и добавляем еще код:
case "SQRT":
    if (arg < 0)
        throw new ArgumentException("Попытка вычислить квадратный корень от отрицательного числа", "arg");
    return Math.Sqrt(arg);

Потом появляются еще функции и свои проверки… В результате метод GetFuncValue напоминает сборную солянку. А хотелось бы как-то попроще и покрасивее сделать.
С помощью делегатов можно сделать так. Описываем делегат, имеющий сигнатуру метода с одним параметром:
        // Делегат- математический метод
        public delegate double MathFunction(double arg);

А теперь описываем словарь соответствия названий и делегатов таких методов:
        // Список функций
        private Dictionary<string, MathFunction> funcList;

Инициализировать его нужно будет в конструкторе:
    /// <summary>
    /// Конструктор
    /// </summary>
    public CallFunc1()
    {
        // Создаем список функций
        funcList = new Dictionary<string, MathFunction>();
        funcList.Add("SIN", delegate(double arg) {
            return Math.Sin(arg);
        });
        funcList.Add("COS", delegate(double arg)
        {
            return Math.Cos(arg);
        });
        funcList.Add("TAN", delegate(double arg)
        {
            return Math.Tan(arg);
        });
        funcList.Add("SQRT", delegate(double arg)
        {
            if (arg < 0)
                throw new ArgumentException("Попытка вычислить квадратный корень от отрицательного числа", "arg");
            return Math.Sqrt(arg);
        });
    }

Обратите внимание, что реализация делегата сделана прямо “по месту” (это называется анонимными методами – методами без имени).
А с помощью LINQ вызов станет совсем простым делом:
    public double GetFuncValue(string funcName, double arg)
    {
        // Ищем нужную функцию в списке
        var func = funcList.Where(f => f.Key == funcName).Select(f => f.Value).FirstOrDefault();
        // Не нашли - генерируем исключение
        if (func == null)
            throw new ArgumentException("Такой функции нет в списке зарегистрированных функций", funcName);
        // Нашли - вызываем эту функцию
        return func(arg);
    }

Теперь каждое имя функции связанно со своей реализацией и своими ограничениями.

Делегаты

В C# нет указателей (про unsafe я молчу). И это хорошо и правильно – указатель он по сути своей не типизирован. Передали в метод ссылку, а как ее интерпретировать – на совести принимающей стороны. Можем передать int, а использовать как float. И т.д. Тоже самое касается и методов – передали указатель на метод с одной сигнатурой, а используем совсем по-другому…
Зато в C# есть делегаты. Я бы назвал делегаты “типизированные указатели на методы”. Покажу на примере как это работает.
Сначала нужно описать тип делегата, т.е. сигнатуру метода. Описание можно не привязывать к конкретному классу:
namespace DelegateTest
{
    public delegate decimal GetVarValue(string name);

Здесь метод имеет один параметр типа string и возвращает decimal.
Теперь этот делегат мы можем использовать при описании параметров и переменных:
public class Parser
{
    /// <summary>
    /// Описание переменной, сохращающей делегат
    /// </summary>
    private GetVarValue getVarValueHandler;

    /// <summary>
    /// Передаем делегат в конструктор
    /// </summary>
    /// <param name="getVarValueHandler"></param>
    public Parser(GetVarValue getVarValueHandler)
    {
        // Сохраняем делегат, т.к. хотим использовать его
        // в другом методе этого класса
        this.getVarValueHandler = getVarValueHandler;
    }

    /// <summary>
    /// Метод расчета
    /// </summary>
    /// <returns></returns>
    public decimal Calc()
    {
        decimal result = 0;

        // Вызываем делегат с параметром var1
        result += getVarValueHandler("var1");
        // Вызываем делегат с параметром var2
        result += getVarValueHandler("var2");

        return result;
    }

Описание переменной типа делегат выглядит совершенно так же как и обычной переменной. Переменная getVarValueHandler имеет тип GetVarValue, т.е. тип делегата с нашей сигнатурой. Отличие от переменной в том, что мы можем вызывать этот метод. Разумеется, вызывать его можно только так, как он описан, т.е. с одним параметром типа string:
                 getVarValueHandler("var1");
Прелесть  в том, что никак иначе вызвать метод по этой ссылке (а передали мы фактически ссылку на метод) нельзя. Только так, как метод описан в описании делегата.
Использовать этот класс тоже очень просто. Во-первых, нам потребуется метод, который мы будем передавать в конструктор нашего класса. Наверное, излишне говорить, что сигнатура этого метода должна совпадать с сигнатурой делегата и передать в конструктор что-то другое нельзя:
class Program
{
    static void Main(string[] args)
    {
        // Передаем в конструктор делегат, т.е. ссылку
        // на наш метод
        Parser parser = new Parser(GetVarValueMethod);
        // Вызываем метод Calc
        Console.WriteLine(parser.Calc());
    }

    /// <summary>
    /// Делегат. Имя у него может быть любое, главное
    /// чтобы сигнатура была той как описана в типе
    /// делегата
    /// </summary>
    static decimal GetVarValueMethod(string name)
    {
        Console.WriteLine("Value of {0}:", name);
        return decimal.Parse(Console.ReadLine());
    }

}

Имя метода может быть может быть любое, главное чтобы сигнатура совпадала с сигнатурой делегата.
Вот и все. Теперь при вызове метода Calc будет два раза вызван метод GetVarValueMethod.

Цитата

студенты: а что вы обычно говорите своим выпускникам, когда встречаете их?
преподаватель: картошку фри и большую колу пожалуйста

Про автомобили и заказчиков

Стал замечать, что очень часто перевожу объяснения нашей работы на примеры более понятые для заказчиков. Например – про автомобили. Объяснять про билды, сборки, тестирование – сложно, а про СТО, автомобили и креш-тесты – как-то ближе получается, проще…
Про олимпиадные задачи и водителей недавно писал… Не для заказчиков правда, для студентов, но все равно как-то доходчивей получается, чем про код и процесс разработки…
Как-то несколько дней просил заказчика прислать нам новые требования. Уже все старые почти сделали, а новых все нет, но точно обещано, что будут. Объясняю – вот если автомобиль разобрали на СТО, чтобы масло поменять в коробке, висит он на подъемнике, поменяли, собрали. И тут вы приходите и говорите что надо бы еще диск сцепления заменить – ну ведь это же все заново разбирать, снова собирать… И мастер ругается словами неприличными и вам потом сборка-разборка в счет пойдет… Другое дело, если заранее сказать, пока все разобрано. Делов-то пару гаек открутить… Так и у нас – пока билд не собрали, пока не оттестировали все – изменения еще можно вносить, а потом это уже двойная работа будет. Надо сказать, что такое объяснение прошло – требования прислали.