Calculations with generic objects

В курса по ООП имахме за домашно следната задача: catrubix

Define a class Matrix<T> to hold a matrix of numbers (e.g. integers, floats, decimals). Implement the operators + and – (addition and subtraction of matrices of the same size) and * for matrix multiplication. Throw an exception when the operation cannot be performed. Implement the true operator (check for non-zero elements).

На пръв поглед – нищо сложно. Създаваме си класа и за всяка операция с матрици създаваме метод, който я извършва. Проблемът идва от там, че generic типовете наследяват System.Object, а при него нямаме имплементация на аритметични операции. Не съществува и някакъв общ интерфейс, който всички числови типове да наследяват. След кратък рисърч намерих няколко статии, които засягат този въпрос и нещото, което ме озадачи, беше че повечето са писани преди повече от 5-6 години. Беше ми трудно да повярвам, че за толкова време няма създаден общопризнат начин по който да се решава този проблем. В тази статия ще разгледам 2 начина за решаване. В единият се пише по-малко и естествено се изпълнява по-бавно, във вторият пишем малко повече, но времето за изпълнение е много по-малко и в реална ситуация би било по-предпочитано.

“The struggle alone pleases us, not the victory.” – Blaise Pascal

Решение с dynamic

Може би вече се досещате – това е мързеливият начин. И естествено този, който аз предадох в домашното. През 2010г. Microsoft добавят към .NET платформата dynamic language runtime(DLR). Той позволява поддръжката на динамично поведение, каквото имаме при Python, Ruby и др. което прави възможно динамичното зареждане и изпълняване на асемблита, обработка на обекти, които не знаем какви са, докато не ги получим, както и създаването на извращения от рода:

using System;
using System.Dynamic;

class Program
{
 static void Main(string[] args)
 {
 dynamic lala = new ExpandoObject();
 lala.Sing = new Action(() => Console.WriteLine("LALA"));
 lala.Sing();
 Console.WriteLine();

 lala.Sing = new Action(() => Console.WriteLine("WOOOF"));
 lala.Sing();
 Console.WriteLine();

 lala.fu = new Action(() => Console.WriteLine("FU"));
 lala.fu();
 Console.WriteLine();

 lala = "this is nonsense";
 Console.WriteLine(lala);
 }
}

От тук нататък по кода няма нищо сложно – създаваме класа Matrix<T>, имплементират се методите и когато трябва да се изчислят самите стойности първо се предават на обект от тип dynamic, рънтайма знае какви са техните типове и ги събира.


 dynamic a = m1[i, j];
 dynamic b = m2[i, j];
 mNew[i,j] = a + b;

Решение с имплементация на интерфейс

Тук се изисква малко повече писане, но се упражняват и използват ключови принципи от ООП. Първото нещо, което трябва да направим е да създадем интерфейс, който да се наследява и за всеки тип данни да се оказва как да се смята.

interface IArithmetic<T>

{
 T Sum(T a, T b);
 T Subtract(T a, T b);
 T Multiply(T a, T b);
 bool IsZero(T a);
}

<T> e generic тип данни, което означава, че когато създаваме обект на негово място можем да сложим тип, какъвто ние пожелаем, но след малко ще видим как ограничаваме това. Обикновено това не се прави, понеже generic типовете се ползват най-често за създаване на различни колекции от данни(List<T>, Dictionary<T, L>), но нашият случай е специален – ние искаме да извършваме аритметични операции с тях и в началото казах, защо това не е съвсем елементарно. Следва да създадем структури, които наследяват нашият интерфейс и указват “как се смята”.


struct IntOperations : IArithmetic<int>
{
 public int Sum(int a, int b) { return a + b; }
 public int Subtract(int a, int b) { return a - b; }
 public int Multiply(int a, int b) { return a * b; }
 public bool IsZero(int a)
 {
  if (a == 0) return true;
  else return false;
 }
}
struct DoubleOperations : IArithmetic<double>
{
 public double Sum(double a, double b) { return a + b; }
 public double Subtract(double a, double b) { return a - b; }
 public double Multiply(double a, double b) { return a * b; }
 public bool IsZero(double a)
 {
  if (a == 0) return true;
  else return false;
 }
}

Тук съм поставил първите две, но има за float и decimal – изглеждат по абсолютно същия начин. Следва дефинирането на класа Matrix.


class Matrix<T, C> where C : IArithmetic<T>, new()
{
 private static C calculator = new C();

....

}

На първият ред се вижда, че приема два параметъра. Първият го имахме и при решението с dynamic – това е типът на числата които ще бъдат в матрицата. На мястото на вторият слагаме някоя от структурите, които създадохме. След това обаче следва ограничението – структурата, която ще използваме задължително трябва да наследява интерфейсът, в който се оказва как се смятат типове T. По този начин можем да създаваме матрици които съдържат различни типове данни – изпълняваме generic условието – и същевременно налагаме методи, които знаят как се работи с конкретният тип.

Предимства и недостатъци

При dynamic се пише много по-малко, което за решаване на домашни го прави автоматичен избор. Лошото е, че е много бавен и ако ни трябва пърформънс определено трябва да се избягва, но определено прави задачи, в които не знаем какво получаваме като тип тривиални за решаване. Друг проблем е, че няма type safety(то остава и да имаше). Тоест можем да сложим в матрицата DateTime, след това да я инициализираме с дати и да се кефим на рънтайм ерорите.

Ако изхождаме от закона на Мърфи, че ако нещо може да се обърка, то винаги се обърква, ще подходим по вторият вариант. Там няма как да сложим някакъв измислен тип, понеже му налагаме ограничения. Дори това – 

Matrix<double, IntOperations> matrix1 = new Matrix<double, IntOperations>(3);

веднага ще изпищи за грешка. Работи много по-бързо, понеже знаем какви са типовете още при самото създаване и няма кастване на обекти. Единственото, което бави е call time към методите, но би трябвало да се инлайнват от JIT компилатора, което на практика означава, че няма да има никакво забавяне, но не съм много сигурен дали методите на статични структури се инлайнват. Като негатив мога да кажа, че се пише повече и инстанцирането на подобен клас е грозно. :D

Заключение

Въпреки, че C# е доста смислен език все още има елементарни неща, които трябва да направим сами. Най-вероятно има причина все още да няма нещо като общ интерфейс за всички числови типове и подобна задача да бъде тривиална. Ако има нещо, което не е обяснено достатъчно добре или не сте разбрали оставете коментар и ще го подробнизирам.

Posted in C#, Programming Tagged with: , , , , , , , , , ,

Leave a Reply

Your email address will not be published.