Blog (9)
Komentarze (73)
Recenzje (0)
@pat.wasiewiczPiszemy trochę bardziej złożony kalkulator w C#.NET

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

17.02.2013 17:25, aktualizacja: 18.02.2013 22:16

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

Wybrane dla Ciebie
Komentarze (6)