В пятницу на очередном занятии возник вопрос - когда лучше делать подклассы, а когда можно обойтись просто свойством (например, сделать тип сущности)? Я так думаю, что если отличий в разных типах не много, то мутить с подклассами не стоит.
Например, если кирпичи в игре Арканоид отличаются только цветом и числом попаданий до уничтожения, то вполне можно обойтись либо типом и тогда возвращать соответсвующие свойства в зависимости от типа:
public Color Foreground()
{
swith (type)
{
case StoneType.One: return Colors.Red;
............
}
Либо вообще вынести эти параметры в конструктор и типы не заводить:
public Stone(Color foreground, int countStrike)
{
.....
}
А вот если отличий будет очень много, то класс будет замусорен кучей if или switch. Вот тогда имеет смысл делать подклассы и разносить логику и код. Разумеется, если какой-то из "типов" имеет дополнительные свойства, то это однозначно нужно выностить в подкласс.
Saturday, February 26, 2011
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.
Цитата
студенты: а что вы обычно говорите своим выпускникам, когда встречаете их?
преподаватель: картошку фри и большую колу пожалуйста
преподаватель: картошку фри и большую колу пожалуйста
Про автомобили и заказчиков
Стал замечать, что очень часто перевожу объяснения нашей работы на примеры более понятые для заказчиков. Например – про автомобили. Объяснять про билды, сборки, тестирование – сложно, а про СТО, автомобили и креш-тесты – как-то ближе получается, проще…
Про олимпиадные задачи и водителей недавно писал… Не для заказчиков правда, для студентов, но все равно как-то доходчивей получается, чем про код и процесс разработки…
Как-то несколько дней просил заказчика прислать нам новые требования. Уже все старые почти сделали, а новых все нет, но точно обещано, что будут. Объясняю – вот если автомобиль разобрали на СТО, чтобы масло поменять в коробке, висит он на подъемнике, поменяли, собрали. И тут вы приходите и говорите что надо бы еще диск сцепления заменить – ну ведь это же все заново разбирать, снова собирать… И мастер ругается словами неприличными и вам потом сборка-разборка в счет пойдет… Другое дело, если заранее сказать, пока все разобрано. Делов-то пару гаек открутить… Так и у нас – пока билд не собрали, пока не оттестировали все – изменения еще можно вносить, а потом это уже двойная работа будет. Надо сказать, что такое объяснение “прошло” – требования прислали.
Subscribe to:
Posts (Atom)