Strona używa cookies (ciasteczek). Dowiedz się więcej o celu ich używania i zmianach ustawień. Korzystając ze strony wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki.    X

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ę. 

windows programowanie

Komentarze

0 nowych
Druedain   13 #1 18.02.2013 13:16

Fajnie, że ktoś wreszcie ciekawiej rozwinął pomysł :)

W niektórych miejscach Ci się wcięcia pokopały. Fajnie jakbyś to jeszcze poprawił.

Autor edytował komentarz.
pat.wasiewicz   5 #2 18.02.2013 17:52

Dzięki, poprawię niebawem ;-)
Mimo, że metoda obliczania całki tylko przykład, to też muszę ją poprawić - z powodu zaokrąglania do całkowitych jest zupełnie bezużyteczna dla odpowiednio 'małych' argumentów.

przemor25   14 #3 18.02.2013 20:59

Jak się okazuje, nawet napisanie najprostszego kalkulatora może nie wydawać się takie proste :) Dzięki za wpis! (:

pat.wasiewicz   5 #4 18.02.2013 22:13

A więcej reszta.. już niedługo :)

Wcięcia poprawione :)

Druedain   13 #5 19.02.2013 11:20

Przeglądałem kilkukrotnie ten kod, próbując go w pełni zrozumieć i powiem Ci, że nie rozumiem co się dzieje http://wklej.to/XoYeq w 19 linii… Mógłbyś to jakoś wytłumaczyć?

Z Calculate wywołujesz Calculate, z którego wywołujesz Calculate… Jedyne zaimplementowane Calculate to z dodawaniem i mnożeniem. Dlaczego to działa? :P

Hmmm, dobra, już chyba wiem jak to działa… Calculate z klasy Constant / Variable.

Autor edytował komentarz.
pat.wasiewicz   5 #6 19.02.2013 12:59

Racja. Wywoływane są przedefiniowane metody Calculation ze Addition, Multiplication, Constant albo Variable.