PROGRAMOWANIE – PRZYKŁADY
IDE C++ WinBGI C# Wpadki

Przykład C#

Suma szeregu liczbowego Funkcja Bessela Program w Visual C# Formatowanie liczb Definiowanie klasy Program w Visual C# (wersja 2) Poprzedni przykład Następny przykład Program w Visual C++ Kontakt

Suma szeregu liczbowego

Szereg geometryczny o nieskończonej liczbie wyrazów, rozpatry­wany w ramach matema­tyki na poziomie licealnym, stanowi szcze­gólny przypadek szeregów liczbowych postaci

który może mieć sumę skończoną lub nie. Gdy w liceum po raz pierwszy zetknąłem się z szeregami geometry­cznymi, zadałem sobie pytanie: jak to możliwe, by suma nieskoń­czonej liczby wyrazów, nawet niewyobra­żalnie małych, była skończona? Moje wątpliwości prysły, gdy pomyślałem o ciągu 1, 1/10, 1/100, ... – sumo­wanie szeregu o tych wyrazach to tylko "dopisywanie" kolejnej "jedynki":

1,11111... = 1,(1)

Rozstrzygnięcie, czy szereg geometryczny jest zbieżny i ma sumę czy nie, jest proste, ale dla innych szeregów może być zadaniem trudnym. W ogólno­ści mówimy, że szereg liczbowy jest zbieżny, gdy ciąg jego sum cząstkowych:

jest zbieżny do pewnej granicy. Wówczas granica ta jest sumą tego szeregu. Natu­ralny schemat obli­czania sumy szeregu liczbo­wego, oczywiście pod warunkiem, że jest on zbieżny, polega na itera­cyjnym obli­czaniu kolejnego składnika i dodawaniu go do wartości zmiennej, w której liczona jest suma:

w = kolejny_składnik;
s = s + w;

Podstawową zasadą, którą należy się kierować przy obli­czaniu składników sumy, jest próba znale­zienia zależności rekuren­cyjnej pomiędzy dwoma kolejnymi składni­kami. Jeżeli taka zależność jest znana, jej wykorzy­stanie prowadzi do zmniej­szenia liczby działań arytmety­cznych potrzebnych do rozwią­zania zadania. Sumowanie należy zakończyć, gdy nieuwzglę­dnione składniki prakty­cznie nie wpływają na wartość sumy. Często nie można z góry określić liczby iteracji, gdyż zależy ona od szybkości zbież­ności szeregu i żądanej dokła­dności wyniku. Zazwyczaj sumowanie kończy się, gdy ostatni składnik uwzglę­dniony w końcowej sumie cząstkowej jest w porównaniu z nią względnie mały, czyli gdy nie prze­kracza tej sumy pomno­żonej przez zadaną dokładność:

Obliczanie sumy szeregu za pomocą komputera wymaga dużej ostrożności, ponieważ zbyt wolna zbież­ność szeregu wymaga uwzglę­dnienia dużej liczby składników, co może prowa­dzić do znacznej kumulacji błędów zaokrą­gleń wynika­jących z reali­zacji operacji arytmety­cznych na liczbach rzeczywistych.

Funkcja Bessela

W przykładowym programie w języku C++ tworzona jest tablica wartości funkcji Bessela pierwszego rodzaju rzędu 0 zdefinio­wanej za pomocą szeregu potęgowego:

w którym

oznacza silnię. Składniki sumy tego szeregu spełniają zależność rekuren­cyjną

z której wynika, że mają one naprzemienne znaki i dążą do zera. Można wykazać, posłu­gując się metodami analizy matematy­cznej, że właści­wość ta gwaran­tuje zbie­żność szeregu dla dowolnej liczby rzeczy­wistej x. Rozwa­żania te prowadzą do następu­jącej metody obli­czania warto­ści funkcji Bessela w języku C#:

double Jo(double x)
{
    double w = 1.0,  s = w;
    for (int k = 1; Math.Abs(w) > EPS * Math.Abs(s); k++)
    {
        w = -0.25 * x * x / (k * k) * w;
        s = s + w;
    }
    return s;
}

Podobnie jak w C++, nagłówek określa, że funkcja zwraca wartości typu double, ma nazwę Jo i jeden argu­ment typu double o nazwie x, który jest trakto­wany jak zmienna lokalna w bloku występu­jącym tuż za nagłówkiem obejmu­jącym w nawiasach {} tzw. ciało metody. Wartość tej zmiennej jest ustalana w trakcie wywołania metody.

Zmienne lokalne w i s są inicjali­zowane składnikiem w0 równym 1. W k-tym kroku iteracji realizo­wanej za pomocą pętli for zmiennej w przypi­sywany jest kolejny skła­dnik wk, zaś zmien­nej s kolejna suma cząstkowa sk. Iteracja jest kończona, gdy ostatni składnik stanowi niezna­czący ułamek sumy cząstkowej (dokła­dność EPS zostanie okre­ślona poza metodą). Wartością zwra­caną przez funkcję Jo jest osta­tnia suma cząstkowa przypi­sana zmiennej s. W warunku konty­nuacji pętli for wystę­puje metoda Abs klasy matema­tycznej Math wyznacza­jąca wartość bezwzględną.

W celu przyśpieszenia obliczeń warto niezmieniającą się wartość wyznacza­nego wielo­krotnie czynnika występu­jącego w wyra­żeniu określa­jącym kolejny składnik obliczyć jeden raz przed rozpo­częciem iteracji i przypisać ją zmiennej pomocni­czej, a nastę­pnie zastąpić nią wystą­pienie tego czynnika w pętli for. Można również zamiast dwóch instrukcji nada­jących zmiennym ws nowe wartości użyć jedną zwartą instru­kcję i pominąć zbędne nawiasy {}:

double Jo(double x)
{
    double w = 1.0,  s = w,  m4xx = -0.25 * x * x;
    for (int k = 1; Math.Abs(w) > EPS * Math.Abs(s); k++)
        s += (w *= m4xx / (k * k));
    return s;
}

Powyższe dwie wersje metody są niemal identyczne jak odpowia­dające im dwie funkcje w C++. W celu zbudo­wania bardziej uniwer­salnego programu rozpatrzmy zadanie obli­czania sumy szeregu potęgo­wego reprezentu­jącego funkcję Bessela pierw­szego rodzaju rzędu n:

(n=0,1,...). Nietrudno sprawdzić, że składniki tego szeregu speł­niają zależność:

Tym razem nie można zmiennych w i s zainicja­lizować składnikem w0, gdyż trzeba go najpierw wyzna­czyć itera­cyjnie. Dalsze obli­czenia przebie­gają już według omówio­nego schematu. Rząd funkcji nie jest jednak znany w trakcie pisania programu, z tego powodu określać go będzie dodatkowy argu­ment n typu int:

double J(int n, double x)
{
    double w = 1.0,  s;
    for (int k = 1; k <= n; k++)
        w = w * x / (2 * k);
    s = w;
    for (int k = 1; Math.Abs(w) > EPS * Math.Abs(s); k++)
    {
        w = -0.25 * x * x / (k * (n + k)) * w;
        s = s + w;
    }
    return s;
}

Również i tę metodę warto usprawnić, przenosząc wyzna­czanie niezmienia­jących się czyn­ników przed pętle for. I tak, przed pierwszą pętlą w zmiennej pomo­cniczej wsk zapamię­tujemy wartość x/2, a przed drugą pętlą, używając tej samej zmiennej, wartość -x2/4. Ponadto zastę­pujemy jedną instru­kcją blok składa­jący się z dwóch instru­kcji przypisu­jących zmien­nym ws kolejny skła­dnik i sumę cząstkową:

double J(int n, double x)
{
    double w = 1.0,  s,  wsp = 0.5 * x;
    for (int k = 1; k <= n; k++)
        w *= wsp / k;
    wsp *= -wsp;
    s = w;
    for (int k = 1; Math.Abs(w) > EPS * Math.Abs(s); k++)
        s += (w *= wsp / (k * (n + k)));
    return s;
}

Program w Visual C#

Aplikacja konsolowa w C# ma tworzyć tablicę wartości kilku funkcji Bessela, załóżmy, że J0, J1J2, w równo­odległych punktach przedziału [0,10]. Rozbu­dowę szablo­nowego kodu źródło­wego rozpoczy­namy od umieszcze­nia w klasie Program omówionej wyżej defi­nicji metody J i pola EPS określa­jącego dokła­dność sumo­wania szeregów równą ½.10-8. Kolejność składowych klasy jest dowolna. Obie składowe poprze­dzamy modyfi­katorem static, który oznacza, że będą one staty­czne, podobnie jak metoda Main, od której rozpo­czyna się wykonanie programu. Metoda ta wypro­wadza do okna konso­lowego nagłówek tablicy i jej wiersze z wartościami trzech funkcji w równo­odległych punktach rozpatry­wanego przedziału:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Bessel1

{
    class Program
    {
        static void Main(string[] args)
        {
            Console.Write("  x       Jo(x)      J1(x)      J2(x)");
            Console.Write("--------------------------------------");
            for (int i = 0; i <= N; i++)
            {
                double x = i * H;
                Console.WriteLine("{0,5:F2} {1,10:F6} {2,10:F6} {3,10:F6}",
                                  x, J(0,x), J(1,x), J(2,x));
            }
            Console.Write("--------------------------------------");
        }

        static double J(int n, double x)
        {
            double w = 1.0,  s,  wsp = 0.5 * x;
            for (int k = 1; k <= n; k++)
                w *= wsp / k;
            wsp *= -wsp;
            s = w;
            for (int k = 1; Math.Abs(w) > EPS * Math.Abs(s); k++)
                s += (w *= wsp / (k * (n + k)));
            return s;
        }

        static int N = 20;
        static double H = 0.5;
        static double EPS = 0.5e-8;
    }
}

Dwa pola statyczne klasy Program parame­tryzują tablicę wynikową: H precy­zuje długość kroku (odle­głość między sąsie­dnimi punktami), a N liczbę wszys­tkich kroków. A oto wynik wyko­nania programu:

Formatowanie liczb

Występujące w programie wywołanie metody WriteLine klasy Console, powodu­jące wyprowa­dzenie wiersza tablicy do okna konsoli, zawiera cztery symbole definiu­jące format wypisy­wanych wartości numery­cznych. Na przykład symbol {2,10:F6} oznacza, że argument o indeksie 2, czyli wartość J(1,x), zostanie wyświe­tlony w polu o szerokości 10 zna­ków jako wyrównana do jego prawego brzegu liczba stałopo­zycyjna z 6 cyframi po prze­cinku. Ogólnie symbol forma­tujący ma postać:

{indeks, wyrównanie: specyfikator_formatu}

Indeks wskazuje numer argumentu, który ma być wstawiony w miejsce symbolu. Argu­menty są nume­rowane od zera. Indeks jest obowią­zkowy, zaś pozostałe parametry nie. Wyrównanie jest liczbą całko­witą określa­jącą minimalną długość tekstu. Jeśli jest dodatnia, tekst jest wyrówny­wany do prawego brzegu pola, a jeśli jest ujemna, to do lewego. Gdy jest pomi­nięta, pomi­nięty powinien być przecinek. Specyfi­kator formatu zawiera kod forma­towania, m.in.:

Po kodzie formatowania może wystąpić liczba całko­wita oznacza­jąca precyzję (gdy jest pomi­nięta, przyjmo­wana jest wartość domyślna). Poniższy prosty program prezen­tuje kilka przykładów formato­wania liczb całko­witych i rzeczy­wistych (usunięto z niego dyrektywy using włącza­jące nieuży­wane przestrze­nie nazw oraz defi­nicję własnej przestrzeni).

using System;

class Program
{
    static void Main(string[] args)
    {
        int k = 2018;
        double x = 43.58764, p = 0.726;
        Console.WriteLine("Walutowy       C: {0:C}", x);
        Console.WriteLine("Naukowy        E: {0:E3}", x);
        Console.WriteLine("Stałopozycyjny F: {0:F3}", x);
        Console.WriteLine("Procentowy     P: {0:P}", p);
        Console.WriteLine("Szesnastkowy   X: {0:X4}", k);
    }
}

Wynik wykonania programu ukazuje następujący rysunek:

Definiowanie klasy

Zajmiemy się obecnie zdefiniowaniem klasy, której nadamy nazwę Bessel. Jej zadaniem będzie obli­czane wartości funkcji Bessela pierw­szego rodzaju rzędu n. Rozpoczy­namy od umieszcze­nia poza klasą Program pustego szkie­letu nowej klasy:

class Bessel
{
}

i przeniesienia do niej metody J i pola EPS z klasy Program. Matema­tycy traktują funkcję Bessela jako funkcję jednej zmiennej x, a jej rząd n jako indeks stano­wiący rozsze­rzenie nazwy. Z tego względu usuwamy argument n metody J i dodajemy w zamian pole n w klasie Bessel. Jego wartość zależy od wyboru funkcji, toteż gdy używa­nych jest kilka funkcji równo­cześnie, czyli kilka obiektów tej klasy, nie może ono być staty­czne, gdyż w przeci­wnym razie byłoby wspólne dla nich wszys­tkich. Konse­kwencją tego ograni­czenia jest to, że metoda J nie może być staty­czna, obowią­zuje bowiem reguła, że metoda staty­czna nie może odwoływać się w swojej klasie do elementów niestaty­cznych. Usuwamy zatem modyfi­kator static z jej nagłówka.

Jedyną możliwością określenia rzędu funkcji Bessela, której wartości mają być obliczane, jest wykorzy­stanie specjalnej metody zwanej konstru­ktorem, która jest wywo­ływana automa­tycznie w momencie tworzenia obiektu w pamięci. Konstru­ktor ma taką samą nazwę jak klasa i nie zwraca żadnej wartości. Definiu­jemy zatem konstru­ktor, który będzie inicjali­zował pole n obiektu warto­ścią argu­mentu. Z przytoczonej wyżej reguły wynika, że i on nie może być staty­czny, zresztą konstru­ktor staty­czny nie może mieć argu­mentów. Aby dostęp do konstru­ktora Bessel i metody J był możliwy spoza klasy, umieszczamy na początku ich nagłówków modyfi­kator public (element publi­czny). Efektem naszych rozważań jest następu­jąca defi­nicja klasy:

class Bessel
{
    public Bessel(int n)
    {
        this.n = n;
    }

    public double J(double x)
    {
        double w = 1.0, s, wsp = 0.5 * x;
        for (int k = 1; k <= n; k++)
            w *= wsp / k;
        wsp *= -wsp;
        s = w;
        for (int k = 1; Math.Abs(w) > EPS * Math.Abs(s); k++)
            s += (w *= wsp / (k * (n + k)));
        return s;
    }

    int n;
    static double EPS = 0.5e-8;
}

Przypomnijmy, że słowo kluczowe this jest referencją do bieżą­cego obiektu (wartością informu­jącą o położeniu obiektu w pamięci). Użyte w konstruktorze klasy Bessel odwo­łanie this.n odnosi się zatem do pola n aktual­nego obiektu tej klasy, zaś n do argu­mentu konstru­ktora. Gdyby użyć innej nazwy argu­mentu, odwołanie n dotyczy­łoby pola obiektu. Inna nazwa argu­mentu lub pola wyraża­jącego rząd funkcji Bessela odbiega­łaby jednak od zwycza­jowego nazewnictwa matematy­cznego. Utra­cony kontekst nie przy­niósłby żadnego pożytku, raczej pogor­szenie zrozu­mienia kodu.

Program w Visual C# (wersja 2)

Zmodyfikowana wersja programu tworzącego tablicę wartości trzech funkcji Bessela J0, J1J2 w punktach dzielą­cych prze­dział [0,10] na odcinki o długości ½, złożona z dwóch klas BesselMain znajdu­jących się w prze­strzeni nazw Bessel2 (ich kolej­ność może być odwrotna), ma postać:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Bessel2
{
    class Bessel
    {
        public Bessel(int n)
        {
            this.n = (n >= 0) ? n : 0;
        }

        public double J(double x)
        {
            double w = 1.0, s, wsp = 0.5 * x;
            for (int k = 1; k <= n; k++)
                w *= wsp / k;
            wsp *= -wsp;
            s = w;
            for (int k = 1; Math.Abs(w) > EPS * Math.Abs(s); k++)
                s += (w *= wsp / (k * (n + k)));
            return s;
        }

        readonly int n;
        static readonly double EPS = 0.5e-8;
    }

    class Program
    {
        static void Main(string[] args)
        {
            Bessel J0 = new Bessel(0);
            Bessel J1 = new Bessel(1);
            Bessel J2 = new Bessel(2);
            Console.WriteLine(" x Jo(x) J1(x) J2(x)");
            Console.WriteLine("--------------------------------------");
            for (int i = 0; i <= N; i++)
            {
                double x = i * H;
                Console.WriteLine("{0,5:F2} {1,10:F6} {2,10:F6} {3,10:F6}",
                                  x, J0.J(x), J1.J(x), J2.J(x));
            }
            Console.WriteLine("--------------------------------------");
        }

        static readonly int N = 20;
        static readonly double H = 0.5;
    }
}

W porównaniu z pierwowzorem w klasie Bessel wprowa­dzono drobne zmiany. I tak, w konstruktorze zabezpie­czono się przed ujemnym rzędem funkcji Bessela, przypi­sując polu n wartość 0 w razie ujemnego argu­mentu (inne rozwią­zanie polega na użyciu typu uint zamiast int dla argu­mentu i pola). Ponadto obydwa pola klasy, nEPS, zostały zgodnie z sugestią kompila­tora zadekla­rowane z użyciem słowa kluczo­wego readonly, czyli jako pola tylko do odczytu. Wartość takiego pola można określić jedynie w jego dekla­racji lub konstru­ktorze. Pomi­nięcie readonly nie jest błędem.

W klasie Main pola N i H precy­zujące liczbę kroków i ich długość również zostały zadekla­rowane jako tylko do odczytu. Istotne zmiany dotyczą utwo­rzenia trzech obiektów klasy Bessel i trzykrotnego wywo­łania metody J tej klasy w odnie­sieniu do każdego z tych obiektów. Przykła­dowo, instrukcja

Bessel J1 = new Bessel(1);

tworzy gdzieś w pamięci komputera obiekt klasy Bessel (instancję tej klasy) i przypisuje zmiennej J1 refe­rencję do niego. Pole n tego obiektu ma wartość 1 nadaną przez konstru­ktor klasy Bessel. Zatem instrukcja

Console.Write("{0,10:F6}", J1.J(x));

wywoła najpierw metodę J klasy Bessel dla obiektu J1, która obliczy wartość funkcji Bessela rzędu 1 (określo­nego w polu n) z dokładnością ½.10-8 (podaną w polu EPS) dla argumentu x, a nastę­pnie wypro­wadzi tę wartość jako liczbę stałopozy­cyjną z 6 cyframi po przecinku.


Opracowanie przykładu: lipiec 2018