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.

r   e   k   l   a   m   a

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