Blog (9)
Komentarze (73)
Recenzje (0)

Piszemy trochę bardziej złożony kalkulator w C#.NET

@pat.wasiewiczPiszemy trochę bardziej złożony kalkulator w C#.NET17.02.2013 17:25

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:

Solution Explorer
Solution Explorer

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:

Dodawanie referencji
Dodawanie referencji

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:

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

Szanowna Użytkowniczko! Szanowny Użytkowniku!
×
Aby dalej móc dostarczać coraz lepsze materiały redakcyjne i udostępniać coraz lepsze usługi, potrzebujemy zgody na dopasowanie treści marketingowych do Twojego zachowania. Twoje dane są u nas bezpieczne, a zgodę możesz wycofać w każdej chwili na podstronie polityka prywatności.

Kliknij "PRZECHODZĘ DO SERWISU" lub na symbol "X" w górnym rogu tej planszy, jeżeli zgadzasz się na przetwarzanie przez Wirtualną Polskę i naszych Zaufanych Partnerów Twoich danych osobowych, zbieranych w ramach korzystania przez Ciebie z usług, portali i serwisów internetowych Wirtualnej Polski (w tym danych zapisywanych w plikach cookies) w celach marketingowych realizowanych na zlecenie naszych Zaufanych Partnerów. Jeśli nie zgadzasz się na przetwarzanie Twoich danych osobowych skorzystaj z ustawień w polityce prywatności. Zgoda jest dobrowolna i możesz ją w dowolnym momencie wycofać zmieniając ustawienia w polityce prywatności (w której znajdziesz odpowiedzi na wszystkie pytania związane z przetwarzaniem Twoich danych osobowych).

Od 25 maja 2018 roku obowiązuje Rozporządzenie Parlamentu Europejskiego i Rady (UE) 2016/679 (określane jako "RODO"). W związku z tym chcielibyśmy poinformować o przetwarzaniu Twoich danych oraz zasadach, na jakich odbywa się to po dniu 25 maja 2018 roku.

Kto będzie administratorem Twoich danych?

Administratorami Twoich danych będzie Wirtualna Polska Media Spółka Akcyjna z siedzibą w Warszawie, oraz pozostałe spółki z grupy Wirtualna Polska, jak również nasi Zaufani Partnerzy, z którymi stale współpracujemy. Szczegółowe informacje dotyczące administratorów znajdują się w polityce prywatności.

O jakich danych mówimy?

Chodzi o dane osobowe, które są zbierane w ramach korzystania przez Ciebie z naszych usług, portali i serwisów internetowych udostępnianych przez Wirtualną Polskę, w tym zapisywanych w plikach cookies, które są instalowane na naszych stronach przez Wirtualną Polskę oraz naszych Zaufanych Partnerów.

Dlaczego chcemy przetwarzać Twoje dane?

Przetwarzamy je dostarczać coraz lepsze materiały redakcyjne, dopasować ich tematykę do Twoich zainteresowań, tworzyć portale i serwisy internetowe, z których będziesz korzystać z przyjemnością, zapewniać większe bezpieczeństwo usług, udoskonalać nasze usługi i maksymalnie dopasować je do Twoich zainteresowań, pokazywać reklamy dopasowane do Twoich potrzeb. Szczegółowe informacje dotyczące celów przetwarzania Twoich danych znajdują się w polityce prywatności.

Komu możemy przekazać dane?

Twoje dane możemy przekazywać podmiotom przetwarzającym je na nasze zlecenie oraz podmiotom uprawnionym do uzyskania danych na podstawie obowiązującego prawa – oczywiście tylko, gdy wystąpią z żądaniem w oparciu o stosowną podstawę prawną.

Jakie masz prawa w stosunku do Twoich danych?

Masz prawo żądania dostępu, sprostowania, usunięcia lub ograniczenia przetwarzania danych. Możesz wycofać zgodę na przetwarzanie, zgłosić sprzeciw oraz skorzystać z innych praw wymienionych szczegółowo w polityce prywatności.

Jakie są podstawy prawne przetwarzania Twoich danych?

Podstawą prawną przetwarzania Twoich danych w celu świadczenia usług jest niezbędność do wykonania umów o ich świadczenie (tymi umowami są zazwyczaj regulaminy). Podstawą prawną przetwarzania danych w celu pomiarów statystycznych i marketingu własnego administratorów jest tzw. uzasadniony interes administratora. Przetwarzanie Twoich danych w celach marketingowych realizowanych przez Wirtualną Polskę na zlecenie Zaufanych Partnerów i bezpośrednio przez Zaufanych Partnerów będzie odbywać się na podstawie Twojej dobrowolnej zgody.