Создаем свой тип, часть 1
- Создаем GUI приложение
- Добавляем класс длина
- Добавляем конструктор
- Приступаем к проверке кода
- Добавляем операции
Задача:
- Создать класс реализующий операции в соответствии с заданием
- Протестировать операции
- Создать GUI приложение, а-ля калькулятор
для следующего задания:
Мера длины, задаваемая в виде пары (значение, тип), допустимые типы: метры, км, астрономические единицы, парсеки
- сложение
- вычитание
- умножение на число
- сравнение двух объемов
- вывод значения в любом типе
Создаем GUI приложение
Выбираем Файл / Создать / Проект и выбираем WindowsForms приложение
видим пустую форму
пока мы не будем ничего с ней делать, наша первоочередная задача — это создать класс на наш новый тип, добавить необходимые операции и протестить код.
Переключимся на код формы (жмем F7) и увидим
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApp10
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
}
}
Добавляем класс длина
В прошлом семестре мы как правило добавляли класс прям в файл с кодом формы, и из-за этого иногда возникали ошибки, связанные то с положением класса в форме, то с тем что класс логики оказывался внутри класса формы и т.п. в общем проблемы было много, а выгоды почти никакой.
Поэтому в этом году мы будем действовать более грамотно и будем создавать наш класс в отдельном файле.
И так, тыкаем правой кнопкой мыши на называние проекта и выбираем Класс…
прописываем имя класса Length и жмем Добавить
Увидим вот это
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WindowsFormsApp10
{
class Length
{
}
}
заменим
class Length
на
public class Length
Добавляем конструктор
У нас свой тип, который есть комбинация более простых типов. Со значением все просто это число, пусть будет типа double, а вот с мерой сложнее. Давайте для начала просто будем хранить меру как строку, типа
- “m” – измеряем в метрах
- “km” – измеряем в километрах
- “au” – астрономическая единица (англ. astronomical unit)
- “pc” – парсек
у нас потребуется два поля:
- double value – под значение
- string type – под тип меры
добавляем
public class Length
{
private double value;
private string type;
public Length(double value, string type)
{
this.value = value;
this.type = type;
}
}
теперь давайте добавим метод, который будет выводить нам значение в читаемом виде:
public class Length
{
private double value;
private string type;
public Length(double value, string type)
{
this.value = value;
this.type = type;
}
// выводит значение в виде (1 km, 2 m, 3 au и т.д.)
public string Verbose() {
return String.Format("{0} {1}", this.value, this.type);
}
}
Приступаем к проверке кода
Так как наше приложение не консольное, но тратить время и пускаться во все тяжкие воюя с интерфейсом лениво. Поэтому мы просто создадим тесты и будем проверять как у нас все работает на тестах.
С небольшой натяжкой такой подход можно назвать TDD (test-driven development)
то есть мы сначала опробуем наш код на тестах, постепенно дорабатывая, а потом уже будем подключать его к интерфейсу
И так, тыкаем где-нибудь в центре класса Length правой кнопкой мыши и выбираем Создание модульных тестов
жмем Ok
получаем что-то в этом роде
using Microsoft.VisualStudio.TestTools.UnitTesting;
using WindowsFormsApp10;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WindowsFormsApp10.Tests
{
[TestClass()]
public class LengthTests
{
[TestMethod()]
public void LengthTest()
{
Assert.Fail();
}
[TestMethod()]
public void VerboseTest()
{
Assert.Fail();
}
}
}
уберем тест LengthTest, и оставим только VerboseTest
[TestClass()]
public class LengthTests
{
[TestMethod()]
public void VerboseTest()
{
Assert.Fail();
}
}
Согласно его имени, он должен проверять как работает функция Verbose, поэтому добавим строки для создания экземпляра нашего класса (он же тип), и проверим что вид работает корректно
[TestMethod()]
public void VerboseTest()
{
var length = new Length(10, "m");
Assert.AreEqual("10 m", length.Verbose());
}
запускаем:
красота:
Давайте теперь подумаем, что у нас сейчас не очень. То есть хотя мы и храним данные для длины, заданной в произвольной мере. Никто не мешает нам указать при создании экземпляра класса произвольный тип, например:
[TestMethod()]
public void VerboseTest()
{
var length = new Length(38, "попугаев");
Assert.AreEqual("38 попугаев", length.Verbose());
}
и если запустить этот код прекрасно выполнится и тест будет зелененький.
“Ну так это здорово” – скажете вы. И я частично с вами соглашусь. Но проблема в том, что тот, кто будет использовать наш класс, сможет указать произвольный тип, и потом, когда придет момент складывать попугаев с астрономическими единицами то будет абсолютно не понятно что делать. Поэтому было б неплохо как-нибудь ограничить список допустимых мер.
Для этого в C# придумали особое ключевое слово enum, который позволяет объявить так называемые перечисляемое значение.
Объявим перечисляемое значения для меры. Переключимся на файл Length.cs, где лежит наш класс и добавим туда:
// добавил перечислимое значение
public enum MeasureType {m, km, au, ps};
public class Length
{
private double value;
private MeasureType type; // << тут заменил string на MeasureType
public Length(double value, MeasureType type) // << и тут тоже заменил string на MeasureType
{
this.value = value;
this.type = type;
}
public string Verbose() {
return String.Format("{0} {1}", this.value, this.type);
}
}
и теперь переключимся на код тестов и подправим:
[TestClass()]
public class LengthTests
{
[TestMethod()]
public void VerboseTest()
{
var length = new Length(38, MeasureType.m);
Assert.AreEqual("38 m", length.Verbose());
}
}
давайте теперь сделаем, чтобы вместо 38 m выводилось по-русски типа 38 метров/километров и т. д. Согласно каноничному TDD, сначала пишется тест, а потом код. Я сам в своей практике, предпочитаю делать эта параллельно. Но в этот раз давайте все-таки сначала поправим тест. И так:
[TestMethod()]
public void VerboseTest()
{
// тестируем все четыре типа
var length = new Length(38, MeasureType.m);
Assert.AreEqual("38 м.", length.Verbose());
length = new Length(38, MeasureType.km);
Assert.AreEqual("38 км.", length.Verbose());
length = new Length(38, MeasureType.au);
Assert.AreEqual("38 а.е.", length.Verbose());
length = new Length(38, MeasureType.ps);
Assert.AreEqual("38 парсек", length.Verbose());
}
если запустить, то увидим ошибку:
ошибка нам не нужна, так что давайте код функции Verbose поправим, переключаемся на Length.cs и правим метод.
Немного колдунства, одна из прелестей enum типов в том, что они отслеживаются компилятором, а, следовательно, для них работает автоподстановка. И можно творить такие чудеса:
ну а теперь редактируем код
public string Verbose() {
string typeVerbose = "";
switch (this.type)
{
case MeasureType.m:
typeVerbose = "м.";
break;
case MeasureType.km:
typeVerbose = "км.";
break;
case MeasureType.au:
typeVerbose = "а.е.";
break;
case MeasureType.ps:
typeVerbose = "парсек";
break;
}
return String.Format("{0} {1}", this.value, typeVerbose);
}
запускаем тесты:
если все сделано корректно, то увидим пройденный результат:
Добавляем операции
Операция сложение с числом
Начнем с простого, с добавления числа. Операция будет работать по следующему правилу: добавление числа к расстоянию увеличивает значение на это число. Например:
- 1 м + 6.25 = 7.25м
- 1а.е. + 4 = 5а.е.
Давайте сначала добавим тест:
[TestClass()]
public class LengthTests
{
[TestMethod()]
public void VerboseTest()
{
// ...
}
[TestMethod()]
public void AddNumberTest()
{
var length = new Length(1, MeasureType.m);
length = length + 4.25;
Assert.AreEqual("5.25 м.", length.Verbose());
}
}
очевидно, что запустить эти тесты мы не сможем, потому что операции сложения с числом у нас пока нет. Поэтому давайте ее добавим, снова идем в файл Length.cs
public class Length
{
//...
public static Length operator+(Length instance, double number)
{
// расчитываем новую значение
var newValue = instance.value + number;
// создаем новый экземпляр класса, с новый значением и типом как у меры, к которой число добавляем
var length = new Length(newValue, instance.type);
// возвращаем результат
return length;
}
// чтобы можно было добавлять число также слева
public static Length operator+(double number, Length instance)
{
// вызываем с правильным порядком аргументов, то есть сначала длина потом число
// для такого порядка мы определили оператор выше
return instance + number;
}
}
запускаем тесты:
красота!
Операции вычитания, умножения, деления с числом
Короче тут все просто, просто значки меняем у функций. По итогу вот такой код добавляем в класс Length
public class Length
{
//...
// умножение
public static Length operator *(Length instance, double number)
{
// мне лень по три строчки писать, поэтому я сокращаю код до одной строки
return new Length(instance.value * number, instance.type); ;
}
public static Length operator *(double number, Length instance)
{
return instance * number;
}
// вычитание
public static Length operator -(Length instance, double number)
{
return new Length(instance.value - number, instance.type); ;
}
public static Length operator -(double number, Length instance)
{
return instance - number;
}
// деление
public static Length operator /(Length instance, double number)
{
return new Length(instance.value / number, instance.type); ;
}
public static Length operator /(double number, Length instance)
{
return instance / number;
}
}
И такие тесты:
[TestClass()]
public class LengthTests
{
//...
[TestMethod()]
public void SubNumberTest()
{
var length = new Length(3, MeasureType.m);
length = length - 1.75;
Assert.AreEqual("1.25 м.", length.Verbose());
}
[TestMethod()]
public void MulByNumberTest()
{
var length = new Length(3, MeasureType.m);
length = length * 3;
Assert.AreEqual("9 м.", length.Verbose());
}
[TestMethod()]
public void DivByNumberTest()
{
var length = new Length(3, MeasureType.m);
length = length / 3;
Assert.AreEqual("1 м.", length.Verbose());
}
}
запускаем:
Преобразование в другой тип
Прежде чем сразу начать писать операцию сложения длин, заданных в разных типах, лучше сначала написать функцию, которая позволит конвертировать один тип в другой.
Самый простой способ — это преобразовывать в какой-нибудь самый привычный тип (например, метры), а затем преобразовывать дальше, и так:
- 1 км == метров
- 1 а.е. == м
- 1 парсек == м (блин, и зачем я парсеки взял… ну ладно лень уже переписывать все)
добавляем метод, который будет называться To (можно было назвать ConvertTo, но это сильно многословно).
Добавим функцию в класс Length
public class Length
{
//...
//новая функция, возвращает тип Length, по имени To, на вход подается тип newType
public Length To(MeasureType newType)
{
// по умолчанию новое значение совпадает со старым
var newValue = this.value;
// если текущий тип -- это метр
if (this.type == MeasureType.m)
{
// а теперь рассматриваем все другие ситуации
switch (newType)
{
// если конвертим в метр, то значение не меняем
case MeasureType.m:
newValue = this.value;
break;
// если в км.
case MeasureType.km:
newValue = this.value / 1000;
break;
// если в а.е.
case MeasureType.au:
newValue = this.value / 149597870700;
break;
// если в парсек
case MeasureType.ps:
newValue = this.value / (3.0856776 * Math.Pow(10, 16));
break;
}
}
return new Length(newValue, newType);
}
}
добавим тест для проверки конвертации из метра в другие величины:
[TestClass()]
public class LengthTests
{
//...
[TestMethod()]
public void MeterToAnyTest()
{
Length length;
length = new Length(1000, MeasureType.m);
Assert.AreEqual("1 км.", length.To(MeasureType.km).Verbose());
length = new Length(149597870700 * 2, MeasureType.m);
Assert.AreEqual("2 а.е.", length.To(MeasureType.au).Verbose());
length = new Length(3 * 3.0856776 * Math.Pow(10, 16), MeasureType.m);
Assert.AreEqual("3 парсек", length.To(MeasureType.ps).Verbose());
}
}
запускаем, проливаем слезы счастья:
и идем дальше.
Давайте подумаем, нам надо уметь преобразовывать из любого типа в любой другой, то есть с учетом 4 типов, и преобразование из себя в самого себя мы считаем за конвертацию. Нам надо добавить еще 3 * 4 case конструкций. И в каждом правильно все рассчитать.
Иначе говоря, перспектива безрадостная. Поэтому воспользуемся головой, и вместо того чтобы решать задачу 12 раз воспользуемся древним математическим подходом: сведения задачи к предыдущей.
То есть вместо того чтобы пытаться понять сколько астрономических единиц в 1.176 парсеках, и долго и нудно считать это на бумажке, мы преобразуем парсеки в метры, а потом уже метры в астрономические единицы. То есть таким образом вместо того чтобы добавлять 12 различных случаев нам придется добавить только 4.
И так, добавляем:
public Length To(MeasureType newType)
{
var newValue = this.value;
if (this.type == MeasureType.m)
{
// ...
}
else if (newType == MeasureType.m) // если новый тип: метр
{
switch (this.type) // а тут уже старый тип проверяем
{
case MeasureType.m:
newValue = this.value;
break;
case MeasureType.km:
newValue = this.value * 1000; // кстати это то же код что и выше, только / заменили на *
break;
case MeasureType.au:
newValue = this.value * 149597870700; // и тут / на *
break;
case MeasureType.ps:
newValue = this.value * (3.0856776 * Math.Pow(10,16)); // и даже тут, просто / на *
break;
}
}
return new Length(newValue, newType);
}
мы уже почти все, добавляем тест:
[TestClass()]
public class LengthTests
{
//...
[TestMethod()]
public void AnyToMeterTest()
{
Length length;
length = new Length(1, MeasureType.km);
Assert.AreEqual("1000 м.", length.To(MeasureType.m).Verbose());
length = new Length(1, MeasureType.au);
Assert.AreEqual("149597870700 м.", length.To(MeasureType.m).Verbose());
length = new Length(1, MeasureType.ps);
Assert.AreEqual("3.0856776E+16 м.", length.To(MeasureType.m).Verbose());
}
}
вы не думайте, что я сразу понял, что 1 парсек выведется как 3.0856776E+16 м, я запустил тест с неправильным значением и просто глянул что выдалось в ошибке:
запускаем и радуемся:
ну и последний момент остается добавить преобразование из любого типа в любой другой. Для этого нам понадобится добавить всего пару строчек. То есть если мы преобразуем не из метров и не в метры. То тогда преобразуй текущее значение сначала в метр, а потом уже в новый тип:
public Length To(MeasureType newType)
{
var newValue = this.value;
if (this.type == MeasureType.m)
{
// ...
}
else if (newType == MeasureType.m)
{
// ...
}
else // то есть не в метр и не из метра
{
newValue = this.To(MeasureType.m).To(newType).value;
// в принципе можно сразу написать
// return this.To(MeasureType.m).To(newType);
// но хорошем тоном считается наличие всего одного return в функции
}
return new Length(newValue, newType);
}
Операция сложения двух разных длин
Ну и вот и добрались до самого интересного. Будем теперь прибавлять астрономические единицы к парсекам, и километры к астрономическим единицам. В общем как душе будет угодно.
В принципе, мы всю сложную работу сделали в предыдущем параграфе. Так что тут нам надо просто оговорить одно правило. Так как при сложении метров с километрами, или километров с метрами не понятно, что мы должны получить в результате, то я буду считать так
- км + м == км
- м + км = м
- а.е. + парске = а.е
- и т. д.
То есть тип длины определяется первым слагаемым в сумме.
И так, добавляем:
public class Length
{
// ...
// сложение двух длин
public static Length operator+(Length instance1, Length instance2)
{
// то есть у текущей длине добавляем число
// полученное преобразованием значения второй длины в тип первой длины
// так как у нас определен operator+(Length instance, double number)
// то это сработает как ожидается
return instance1 + instance2.To(instance1.type).value;
}
// вычитание двух длин
public static Length operator -(Length instance1, Length instance2)
{
// тут все тоже, только с минусом
return instance1 - instance2.To(instance1.type).value;
}
}
ну и напоследок добавим тесты:
[TestClass()]
public class LengthTests
{
// ...
[TestMethod()]
public void AddSubKmMetersTest()
{
var m = new Length(100, MeasureType.m);
var km = new Length(1, MeasureType.km);
Assert.AreEqual("1100 м.", (m + km).Verbose());
Assert.AreEqual("1.1 км.", (km + m).Verbose());
Assert.AreEqual("0.9 км.", (km - m).Verbose());
Assert.AreEqual("-900 м.", (m - km).Verbose());
}
}
и наслаждаемся:
Чет много получилось. Так что на сегодня все. Как построить GUI приложение а-ля калькулятор расскажу во 2-ой части.