Piszemy trochę bardziej złożony kalkulator w C#.NET
Po przeczytaniu wpisu matrix012345-a Piszemy prosty kalkulator w C#.NET - moja wersja postanowiłem pójść jeszcze troszeczkę o krok dalej. Zamierzam się skupić bardziej na logice niż GUI - dlatego kalkulator będzie działał w konsoli. Całość natomiast będzie skompilowana do assembly w postaci dll, którą będzie można wykorzystać w innych projektach albo rozbudować. Do tego mój prosty kalkulator będzie działa na bazie funkcji - tj. każde wyrażenie będzie reprezentowane przez funkcję stałą albo z jedną zmienną. Dojdzie możliwość całkowania i różniczkowania.
Przed przystapieniem do czytania, polecam zapoznać się co nieco informacjami na temat dziedziczenia (link ) oraz idei klas abstrakcyjnych (link ). Będe wykorzystywał tutaj te zagadnienia programowania obiektowego.
Stwórzy pusty projekt z aplikacją konsolową (moja nazwa projektu [oraz całej solucji]): SampleExpression.
Tworzenie programu podzielę na 3 etapy:
- Reprezentowanie funkcji jednej zmiennej za pomocą własnej klasy Expression
- Parsowanie wyrażenia zapisanego w postaci ONP (Odwrotnej Notacji Polskiej) do klasy Expression
- Parsowanie wyrażenia zapisanego infiksowo (jak to zwykle robi się na matematyce) do postaci ONP
Część 1: klasa Expression
Mój kalkulator będzie działał na bazie funkcji z jedną zmienną. Funkcja będzie reprezentowana przez obiekt klasy Expression oraz jej pochodne. Sama klasa będzie abstrakcyjna - nie będzie można tworzyć jej instacji. Zamierzam zrobić kilka pochodnych klasy Expression, które będą reprezentować: stałą, zmienną, wyrażenie dwu-argumentowe (również abstrakcyjna) oraz pochodne klasy wyrażenia dwu-argumentowego - dodawnie i mnożenie. Każde wyrażenie będzie można wyliczyć, podając wartość zmiennej "x" (czyli obliczyć funkcję w konkretnym punkcie), obliczyć pochodną oraz całkę na podanym przedziale z "jakąś" dokładnością.
Dodajmy do naszej solucji SampleExpression nowy projekt typu Portable Class Library. W moim przypadku, nazwałem go SampleExpressionLibrary. Do nowego projektu dodałem dwa katalogi: Expressions oraz BinaryExpressions:
Zacznijmy od klasy abstrakcyjnej Expression która będzie bazą dla wszystkich pochodnych. Umieszamy ją w wspomnianym katalogu Expresions:
using System; namespace SampleExpressionLibrary.Expressions { public abstract class Expression { } }
Na początek, każde wyrażenie będzie można obliczyć w konkretym punkcie. Jednak narazie nie wiemy jak (tzn sposób wyliczenia wyrażenia jest różnych dla różnych pochodnych klas - inaczej będziemy liczyć wartość dla stałej, zmiennej i wyrażenia dwu-argumentowaego). Zdefiniujmy nasze wyliczenie jako metodę abstrakycjną, którą potem będziemy przedefiniowywać w zależności od konkretnego typy wyrażenia (tzn klasy potomnej dla Expression). Napiszmy nagłówek abstrakcyjnej metody:
public abstract double Calculate(double? point = null);
Drobna uwaga tedchniczna i projektowa: znak zapytanie informuje, że dopuszczamy wartości null (więcej na msdn ). Wartość przyda nam się do obliczania wartości zmiennej dla funkcji stałych (np f(x) = 3 albo f(x) = 5+6*(5-2)'). Zakładamy, że jeżeli wyrażenie będzie zawierało zmienną "x" a my wyliczymy jej wartość z nullem - wywołamy wyjątek. Domyślnie, jak nikt nie poda wartości parametru point zostanie użyty null.
Skoro Expression można już wyliczyć (implementacją Calculate zajmiemy się już osobno w każdej potomnej klasie) możemy wyliczyć całkę - sposób jej liczenia jest jeden dla wszystkich wyrażeń, więc definiujemy metodę:
public double Integral(double from, double to) { //dokładność var step = (to - from) / 100d; var result = 0d; double i = from; while (i < to) { result += Calculate(from + i*step)*step; i += step; } return result; }
Nie zagłębiam się w dokładność całkowania, złożność ani tym podobnym.
Pochodnej też nie obliczym - (zrobią to "za siebie" potomne klasy) więc zdefiniujemy metodę abstrakycjną:
public abstract Expression Derivative();
Chcielibyśmy również do naszego wyrażenia dodawać inne wyrażenia oraz mnożyć przez inne:
public virtual Expression Add(Expression expr) { throw new NotImplementedException(); } public Expression Multiply(Expression expr) { throw new NotImplementedException(); }
Dodawanie/mnożenie będzie podobne dla wszystkich wyrażeń, więc napiszemy ciało tych funkcji. Narazie nie możemy tego zrobić, zajmiemy się tym potem. Całość pliku Expression.cs (jak do tej pory):
using System; using SampleExpressionLibrary.BinaryExpressions; namespace SampleExpressionLibrary.Expressions { public abstract class Expression { public virtual Expression Add(Expression expr) { throw new NotImplementedException(); } public Expression Multiply(Expression expr) { new NotImplementedException(); } public double Integral(double from, double to) { //dokładność var step = (to - from) / 100d; var result = 0d; double i = from; while (i < to) { result += Calculate(from + i*step)*step; i += step; } return result; } public abstract Expression Derivative(); public abstract double Calculate(double? point = null); public abstract override string ToString(); } }
Ostatnia metoda ToString() wymusza w każdej klasie zaimplementowanie konwersji wyrażenia do postaci znakowej - dzięki temu będziemy mogli "obrazowo" zobaczyć wyrażenie. Słówko kluczowe override informuje, że nadpisujemy metodę ToString() klasy Object, po której dziedziczy Expression (oczywiście niejawnie).
Kolejnym krokiem jest zdefiniowanie "bazy" wyrażenia dwu-argumentowego, którego nazwałem BinaryExpression w pliku BinaryExpression w katalogu BinaryExpressions. BinaryExpression jest samo w sobie wyrażeniem, więc dziedziczy po klasie Expression.
Użytkownik korzystający z naszych wyrażeń, nie będzie miał dostępu do tej klasy (dzięki słówkowi kluczowego internal). Będzie korzystał z nich poprzez metody .Add oraz .Multiply klasy Expression. Nowa klasa również nadal jest abstrakcyjna, bo nie wiemy, o jakie działanie dwu-argumentowe chodzi (nie możemy więc zaimplementować Calculate) Musimy za to pamiętać argumenty, dla których jest liczone, więc dodajmy zmienne chronione wraz z konstruktorem, dzięki któremu przypiszemy te wartości:
using SampleExpressionLibrary.Expressions; namespace SampleExpressionLibrary.BinaryExpressions { internal abstract class BinaryExpression: Expression { protected readonly Expression FirstExpression, SecondExpression; protected BinaryExpression(Expression first, Expression second) { FirstExpression = first; SecondExpression = second; } } }
Zdefiniujmy podstawowie dwie operacje dwu-argumentowe (odpowiednio w plikach Addition.cs i Multiplication.cs w katalogu BinaryExpressions):
using System.Text; using SampleExpressionLibrary.Expressions; namespace SampleExpressionLibrary.BinaryExpressions { internal class Addition: BinaryExpression { public Addition(Expression first, Expression second) : base(first, second) { } public override Expression Derivative() { return FirstExpression.Derivative().Add(SecondExpression.Derivative()); } public override double Calculate(double? point = null) { return FirstExpression.Calculate(point) + SecondExpression.Calculate(point); } public override string ToString() { var sb = new StringBuilder(); sb.Append("( "); sb.Append(FirstExpression); sb.Append(" + "); sb.Append(SecondExpression); sb.Append(" )"); return sb.ToString(); } } }
Kilka uwag: Pochodna - wyliczenie łatwe: bierzemy pochodną pierwszego argumentu i dodajemy do niej pochodną drugiego argumentu. Wyliczenie: równie proste - wyliczamy wartość pierwszego argumentu i dodajmy do niej wartość wyliczoną w drugim argumencie. Podobnie definiujemy mnożenie:
using System.Text; using SampleExpressionLibrary.Expressions; namespace SampleExpressionLibrary.BinaryExpressions { internal class Multiplication: BinaryExpression { public Multiplication(Expression first, Expression second) : base(first, second) { } public override Expression Derivative() { return FirstExpression.Derivative() .Multiply(SecondExpression) .Add(SecondExpression.Derivative().Multiply(FirstExpression)); } public override double Calculate(double? point = null) { return FirstExpression.Calculate(point)*SecondExpression.Calculate(point); } public override string ToString() { var sb = new StringBuilder(); sb.Append(FirstExpression); sb.Append(" * "); sb.Append(SecondExpression); return sb.ToString(); } } }
Mając już wyrażenie reprezentujące dodawanie i mnożenie, możemy uzupełnić klasę Expression, tj. metody Add i Multiply:
public virtual Expression Add(Expression expr) { return new Addition(this, expr); } public Expression Multiply(Expression expr) { return new Multiplication(this, expr); }
Jedyne, co nam zostało, to zdefiniowanie stałej i zmiennej. W zmiennej iwykorzystamy wzorzec singleton, tzn. każda zmienna będzie reprezentowana przez jedną instancję klasy Variable. Natomiast dodając statyczną metodę do klasy Constant ułatwimy tworzenie stałych użytkownikowi. Stała
using System; namespace SampleExpressionLibrary.Expressions { public class Constant: Expression { private double _value; protected Constant(double value) { _value = value; } public static Constant GetConstant(double value) { return new Constant(value); } public override Expression Derivative() { return GetConstant(0); } public override double Calculate(double? point = null) { return _value; } public override string ToString() { return String.Format("{0:0.00}", _value); } } }
Definiujemy klasę Variable w katalogu Expressions następująco:
using System; namespace SampleExpressionLibrary.Expressions { public class Variable: Expression { private Variable() { } private static Variable _var; public static Variable GetVariable { get { return _var ?? (_var = new Variable()); } } public override Expression Derivative() { return Constant.GetConstant(1); } public override double Calculate(double? point = null) { if (point == null) throw new ArgumentNullException("Wartość zmiennej nie może być pusta!"); return (double) point; } public override string ToString() { return "x"; } } }
Gotowe!
Użycie
Wróćmy do projektu SampleExpression. Musimy dodać referencję do naszego SampleExpressionLibary, żeby z niej korzystać. Zaznaczamy ten że [SampleExpression] projekt w naszym "Solution Explorer". Następnie wybieramy z menu głównego Project->Add Referencje. W okienku wybieramy zakładę Solution i naszą SampleExpressionLibrary:
Zatwierdzamy wybór.
Możemy teraz wpisać przykładowy kod pliku Program.cs:
using System; using SampleExpressionLibrary.Expressions; namespace SampleExpression { class Program { public static void Main(string[] args) { Expression expr = Constant.GetConstant(5).Add(Variable.GetVariable).Multiply(Variable.GetVariable); Console.Write("Wyrażenie wygląda tak: {0}\nJego wartość dla x=4 to {1}\n" + "Pochodna to {2}", expr, expr.Calculate(4), expr.Derivative()); Console.ReadKey(); } } }
Da nam to taki wynik:
Jak widać działa, jednak należałoby poprawić dwie rzeczy - mnożenie przez 1 oraz dodawanie 0. Jednak to pozostawiam dla chętnych, w ramach ćwiczeń :) Polecam również spróbowanie dodania dzielenia i potęgowania.
Jak wspomniałem wyżej, w kolejnej części pokażę, jak można przeszktałcić wyrażenie w ONP do naszej klasy Expression, a następnie wyrażenie infiksowe do ONP.
Link do całej solucji, która zawiera rozwiązanie problemu dodwania zera i mnożenie przez jedynkę znajdziemy tutaj.
Dzięki za uwagę.