Dziedziczenie

Tworzenie podklasy jest łatwe; musimy po prostu wywołać funkcję setClass tak jak poprzednio i wysłać parametr contains z nazwą klasy, z której będzie dziedziczyć. S4 obsługuje wielokrotne dziedziczenie, ale nie jest to coś, czemu będziemy się przyglądać. Zainteresowanych czytelników zachęcamy do zajrzenia do dokumentacji. Ciekawą cechą klas S4 jest to, że jeśli klasa rozszerza jeden z podstawowych typów R, pojawi się slot o nazwie .Data zawierająca dane z podstawowego typu obiektu. Kod, który działa na podstawowym typie obiektu, będzie działał bezpośrednio na części .Data obiektu, dzięki czemu nasze programowanie będzie nieco łatwiejsze:

setClass(„S4Square”,contains = „S4Rectangle”)

Zwróć uwagę, że kiedy tworzymy instancję klasy S4Square, będziemy musieli przekazać oba atrybuty dla długości i upewnić się, że są takie same. Jak widać, klasa obiektu jest poprawnie identyfikowana, a zdefiniowana wcześniej metoda polimorficzna S4print działa w porządku:

S4_square <- new („S4Square”, a = 4, b = 4, olor = new(„S4Color”, color = „red”))

class(S4_square)

#> [1] “S4Square”

#> attr (, “pakiet”)

#> [1] “.GlobalEnv”

S4print(S4_square)

#> [1] “czerwony prostokąt: 4 x 4 == 16”

Ponownie, dla kompletności, zastępujemy metodę S4print() taką, która używa słowa „square”  zamiast tego i widzimy, że działa zgodnie z oczekiwaniami:

setMethod(„S4print”, „S4square”, function(Self) {

print(paste (

S4color(self), „square:”,

self@a,”x”, self@b . „==”, S4area(self)

))

})

#> [1] “S4print”

S4print(s4_square)

#> [1] “czerwony kwadrat: 4 x 4 == 16”

Hermetyzacja i zmienność

Teraz przyjrzymy się koncepcjom enkapsulacji i zmienności w S4. Po pierwsze, zauważ, że używamy metody print(), a nie S4print(), ponieważ drukujemy określone sloty z S4_rectangle. Jak widać, jeśli nie będziemy ostrożni, nadal możemy przypisać wartości bezpośrednio do elementów wewnętrznych obiektu. Ponownie, nie powinieneś tego robić. Zauważ również, że jeśli użyjemy metody S4color(), którą stworzyliśmy wcześniej, do hermetyzacji dostępu do atrybutu color􀁓, otrzymamy błąd informujący nas, że S4color <- Nie można znaleźć funkcji . To podpowiada nam, że możemy stworzyć taką funkcję i możemy:

print(S4_rectangle@a)

#> [1] 2

S4_rectnagle@a <- 1

print(S4_rectnagle@a)

#> [1] 1

print(S4color(S4_rectangle))

#> [1] “niebieski”

S4color(S4_rectangle) <- „black”

#> Błąd w S4color (S4_rectangle) <- “black”:

nie można znaleźć funkcji „S4color <-”

print(S4color(S4_rectangle))

#> [1] “blue”

Aby stworzyć funkcję, która będzie hermetyzować dostęp do atrybutu obiektu, możemy użyć funkcji setReplaceMethod(), tak jak zrobiliśmy to z funkcją setMethod() wcześniej. Zwróć uwagę, że nazwa metody, którą przekazujemy do funkcji setGeneric, to ta, która została nam zasugerowana w błędzie R, który jest nazwą gniazda, po którym następuje normalny operator przypisania w R, <-. Zwróć również uwagę, że nie ma spacji między nazwą zmiennej a symbolami dla operatora przypisania. Na koniec zwróć uwagę, że podczas przypisywania nowej wartości do gniazda color utworzyliśmy obiekt typu S4Color. Jeśli spróbujesz po prostu przypisać ciąg, tak jak to zrobiliśmy z klasą S3, pojawi się błąd informujący, że próbujesz zrobić coś, czego nie powinieneś robić. Jest to duża zaleta podczas pracy z S4, ponieważ może zapobiec popełnieniu nieoczekiwanych błędów:

setGeneric(„S4color<-„, function(self, value) {

standardGeneric(„S4color<-„)

})

#> [1] “S4color <-“

setReplaceMethod(„S4color”, „S4Rectangle”, function(self, value) {

self@color <- new(„S4Color”, color = value)

return(self)

})

#> [1] “S4color <-“

Kiedy już stworzymy taką metodę, możemy jej użyć do bezpośredniego przypisania do obiektu koloru, w sposób hermetyzowany, co jest znacznie lepsze niż bezpośrednie manipulowanie gniazdami. Jak widać, zmiana koloru jest trwała:

print(S4color(S4_rectangle))

#> [1] “blue”

S4color(S4_rectangle) <- „black”

print(S4color(S4_rectangle))

#> [1] “black”

Metody publiczne i polimorfizm

Ponieważ S4 również używa polimorfizmu parametrycznego (metody należą do funkcji, a nie klas) i już to wyjaśniliśmy już kilka razy wcześniej, w tym miejscu wskażemy tylko różnice z S3. Po pierwsze, zamiast używać funkcji USeMethod() do rejestrowania metod w R, używamy funkcji setGeneric() z nazwą metody i funkcja, która wywołuje funkcję standardGeneric() wewnątrz. Zapewni to mechanizm wysyłania obiektów S4. Aby faktycznie utworzyć metodę, zamiast korzystać z konwencji nazewnictwa, jak to robimy w S3, w rzeczywistości przekazujemy nazwę klasy i metodę do funkcji setMethod(), a także funkcję, która powinna być stosowana jako metoda. Po drugie, kolejność ma znaczenie. Jeśli wywołasz funkcję setMethod() przed wywołaniem metody setGeneric(), mechanizm wysyłania nie zadziała. Zrobiliśmy to w S3, ale tutaj musimy odwrócić kolejność. Na koniec zwróć uwagę, że dostęp do atrybutów obiektów (gniazd) uzyskujemy za pomocą symbolu @, jak wspomnieliśmy wcześniej. Dla kompletności w przykładzie, aby czytelnik mógł porównać kod dla wszystkich trzech przykładów obok siebie, pokażemy teraz, jak zaimplementować ten sam kod, który pokazaliśmy dla przypadku S3:

setGeneric(„S4area”, function(self) {

standardGeneric(„S4area”)

})

#> [1] “S4area”

setMethod(„S4area”, „S4Rectnagle”, function(self) {

return(self@a * self@b)

})

#> [1] “S4area”

S4area(S4_rectangle)

#> [1] 6

setGeneric(„S4color”, function(self) {

standardGeneric(„S4color”)

#> [1] “S4color”

setMethod(„S4color”, „S4Rectagle”, function(self) {

return(self@color@color)

})

#> [1] “S4color”

Jeśli użyjesz print() na S4_rectangle, zobaczysz, że jest to rozpoznawane jako pewny wpis, i pokaże swoje gniazda:

print*S4_rectangle)

#> Obiekt klasy „S4Rectangle”

#> Boks „a”:

#> [1] 2

#>

#> Boks „b”:

#> [1] 3

#>

#> „Kolor” boksu:

#> Obiekt klasy „S4Color”

#> „Kolor” boksu:

#> [1] “niebieski”

Jeśli chcemy zmienić to wyjście, nadpisujemy tę metodę naszą własną, tak jak to zrobiliśmy w przypadku S3. Jeśli jednak to zrobimy, będziemy mieć zdefiniowaną funkcję print() do pracy z obiektami S4 i przestanie ona działać dla obiektów z innych modeli obiektów. Zachęcamy do samodzielnego wypróbowania, zmieniając poniższy kod tak, aby używał wywołania metody print zamiast nazwy S4print. Jak widać, używamy tego samego mechanizmu nadpisywania co poprzednio, więc pominiemy jego wyjaśnienie:

setGeneric(„S4print”, function(self) {

standardGeneric(„S4print”)

})

#> [1] “S4print”

setMethod(„S4print”, „S4Rectangle”, function(self) {

print(paste (

S4color(self), „rectangle:” , self@a, „x”. self@b . „==”, S4area(self)

))

})

#> [1] “S4print”

Teraz możemy użyć metody S4print() do wyświetlenia żądanego wyniku, jak widać w następującym kodzie:

S4print(S4_rectangle)

#> [1] “niebieski prostokąt: 2 x 3 == 6”

Klasy, konstruktory i kompozycja

Klasa S4 jest tworzona za pomocą funkcji setClass(). Jako minimum należy podać nazwę Class i jej atrybuty, formalnie znane jako gniazda w S4. Gniazda są określane w funkcji representation(), a fajną cechą jest to, że określasz typ oczekiwany dla takich atrybutów. To trochę pomaga przy sprawdzaniu typu. Istnieją inne wbudowane funkcje, których nie zamierzamy tutaj przeglądać. Na przykład możesz udostępnić funkcję, która sprawdza, czy obiekt jest spójny (nie został zmieniony w nieoczekiwany sposób). Możesz także określić wartości domyślne w parametrze o nazwie protoype. Jeśli chcesz te funkcje w S3, możesz również zaimplementować je samodzielnie, ale nie są one wbudowane. S4 jest uważany za potężny model obiektowy i zdecydowanie powinieneś zbadać go dokładniej, przeglądając jego dokumentację. Cały kod związany z S4 jest przechowywany w pakiecie metod. Ten pakiet jest zawsze dostępny, gdy uruchamiasz R interaktywnie, ale może być niedostępny, gdy uruchamiasz R w trybie wsadowym. Z tego powodu dobrym pomysłem jest dołączenie jawnego wywołania library(methods) za każdym razem, gdy używasz S4. Jak widać, koncepcyjna różnica w stosunku do klas S3 polega na tym, że tutaj faktycznie określamy typ obiektu dla każdego gniazda. Inne zmiany są bardziej syntaktyczne niż koncepcyjne. Zauważ, że możesz użyć nazwy innej klasy S4 dla jednego z gniazd, tak jak robimy w przypadku color dla S4Rectnagle. Oto jak możesz osiągnąć kompozycję za pomocą S4:

library(methods)

setClass(

Class = „S4olor”

representation = representation(

color = „charater”

)

)

setClass(

Class = „S4Rectangle”,

representation = representation(

a =  „numeric”

b = „numeric”,

color = „S4Color’

)

)

Konstruktor jest tworzony automatycznie przez wywołanie funkcji nw(). Jeśli możesz zobaczyć, po prostu musisz przejść przez nazwę klasy, której instancję tworzysz, oraz wartości, które należy przypisać do slotów:

S4_rectangle <- new(

„S4Rectangle”,

a =2,

b = 3,

color = new(„S4Color, color = „blue”

)

class(S4_rectnagle)

#> [1] “S4Rectangle”

#> attr (, “package”)

#> [1] “.GlobalEnv”

str(S4_rectangle)

#> Klasa formalna „S4Rectangle” [pakiet „.GlobalEnv”] z 3 gniazdami

#> .. @ a: num 2

#> .. @ b: num 3

#> .. @ color: Klasa formalna „S4Color” [pakiet „.GlobalEnv”] z 1 gniazdem

#> .. .. .. @ color: chr “niebieski”

Tak jak poprzednio, pobieramy klasę obiektu i drukujemy ją. Kiedy go drukujemy, widzimy strukturę, która zawiera kilka symboli @. Są to operatory używane do uzyskiwania dostępu do gniazd (zamiast operatora $ dla S3). Możesz również zobaczyć zagnieżdżony slot dla atrybutu color klasy Color:

Niektóre nazwy slotów są zabronione, ponieważ są to zarezerwowane słowa kluczowe w R. Nazwy zabronione to class,comment, dim, dimnames, names,row.names i tsp.

Model obiektowy S4

 

Niektórzy programiści uważają, że S3 nie zapewnia bezpieczeństwa normalnie związanego z programowaniem obiektowym. W S3 bardzo łatwo jest utworzyć klasę, ale może również prowadzić do bardzo zagmatwanych i trudnych do debugowania kodu, gdy nie jest używany z wielką ostrożnością. Na przykład możesz łatwo przeliterować imię, a R nie będzie narzekać. Możesz łatwo zmienić klasę na obiekt, a R też nie narzekał. Klasy S4 zostały opracowane po S3 w celu zwiększenia bezpieczeństwa. S4 zapewnia ochronę, ale wprowadza również dużo gadatliwości, aby zapewnić to bezpieczeństwo. Model obiektowy S4 implementuje większość cech współczesnych zorientowanych obiektowo języków programowania – formalne definicje klas, dziedziczenie, polimorfizm (parametryczny) i hermetyzacja

Dziedziczenie

Klasom S3 brakuje struktury, którą zwykle można znaleźć w innych językach. Dziedziczenie jest realizowane nieformalnie, a enkapsulacja nie jest wymuszana przez język, jak widzieliśmy wcześniej. Aby zaimplementować dziedziczenie, utworzymy funkcję square_constructor, która otrzyma długość boków w a i nazwę koloru. Następnie użyjemy rectangle_constructor() i wyślemy a dla obu długości (tworząc kwadrat), a także wyśle ​​kolor. Następnie dodamy klasę S3square, a na koniec zwrócimy utworzony obiekt:

square_onstructor <- function (a, color) {

square <- rectangle_constructor(a,a, color)

class(square) <- c(„S3square”, class(square))

return(square)

}

Teraz utworzymy kwadrat i wydrukujemy jego klasy. Jak widać, ma przypisane klasy S3square i S3Rectangle w kolejności, a kiedy użyjemy na nich metody print(), w rzeczywistości otrzymujemy funkcję wyświetlania z klasy S3Rectangle, której oczekuje się, ponieważ sygnalizujemy dziedziczenie:

S3_square <- square_constructor(4, „red”)

class(S3_square)

#> [1] “S3Square” “S3Rectangle”

print(S3_square)

#> [1] “czerwony prostokąt: 4 x 4 == 16”

Jeśli chcemy zapewnić określoną funkcjonalność drukowania dla kwadratu, musimy nadpisać metodę print() naszą własną definicją klas S3Square, tak jak to robimy teraz. Funkcja jest dokładnie taka sama jak poprzednio, ale używamy słowa „square” zamiast „rectnagle”.

print.S3square <- function(square) {

print(paste(

S3color(square) , „square:” ,

square$a, „x”, square$b, „==”, S3area(square)

))

}                                                

Teraz, kiedy wyświetlamy, widzimy, że używana jest właściwa metoda, ponieważ w wyniku widzimy słowo „square”. Zauważ, że nie musieliśmy ponownie rejestrować metody print() z funkcją UseMethod(), ponieważ już to zrobiliśmy:

print(S3_square)

#> [1] “czerwony kwadrat: 4 x 4 == 16”

Na koniec pamiętaj, że jeśli atrybut class jest wektorem z więcej niż jednym elementem, to pierwszy element jest interpretowany jako klasa obiektu, a kolejne elementy są interpretowane jako klasy, z których dziedziczy obiekt. To sprawia, że ​​dziedziczenie jest własnością obiektów, a nie klas, a porządek jest ważny. Gdybyśmy zamiast tego napisali clsss(square) <- c(class(square), „S3quare”)w funkcji square_constructor(), to nawet po utworzeniu funkcji print.S3square() będziemy nadal zobacz metodę print() wywołującą funkcję print.S3Rectangle. Uważaj z tym

Hermetyzacja i zmienność

Teraz zobaczymy, jak S3 obsługuje zmienność i hermetyzację. Aby to zrobić, wydrukujemy wartość- w prostokącie, zmodyfikujemy ją i wydrukujemy ponownie. Jak widać, jesteśmy w stanie go zmodyfikować i od tego momentu otrzymujemy inny wynik i robimy to bez wywoływania metod. To bardzo ryzykowna rzecz i zdecydowanie powinieneś zawrzeć tego typu zachowanie w wywołaniach metod:

print(S3_rectangle$a)

#> [1] 2

S3_rectangle$a  <- 1

print(S3_rectangle$a)

#> [1] 1

Nawet jeśli możesz, nigdy nie modyfikuj bezpośrednio elementów wewnętrznych obiektu.

Właściwym sposobem modyfikowania obiektu byłoby użycie jakiejś funkcji ustawiającej. Metoda set_color.S3Rectnagle zostanie użyta do zmodyfikowania koloru prostokąta, otrzymując S3Rectnagle i a string new_color i zapisanie tego nowego łańcucha wewnątrz atrybutu color w prostokącie. Kiedy używasz takiej metody, jasno wyrażasz swoje zamiary, co jest znacznie lepszym sposobem programowania. Oczywiście musimy również zarejestrować wywołanie metody za pomocą R, jak pokazano wcześniej:

set_color.S3Rectangle <- function(rectangle, new_color) {

rectangle$olor <- new_color

return(rectnagle)

}

set_color <- function(object, new_color) {

UseMethod(„set_color”)

}

Czy zauważyłeś nasz błąd? Prawdopodobnie nie, ale świetnie, gdybyś to zrobił! Zrobiliśmy to celowo, aby pokazać, jak łatwo jest wyrządzić sobie krzywdę programując w R. Ponieważ R nie ma funkcji sprawdzania typu, przypadkowo przypisaliśmy łańcuch, do którego powinniśmy przypisać Color. Oznacza to, że atrybut color w naszym prostokącie nie będzie już rozpoznawany jako klasa Color po wywołaniu metody set_color() zostanie rozpoznany jako ciąg. Jeśli twój kod zależy od tego, czy obiekt jest typu Color, prawdopodobnie zakończy się niepowodzeniem w nieoczekiwany i mylący sposób i będzie trudny do debugowania. Zachowaj ostrożność podczas wykonywania zadań. Zamiast tego powinniśmy umieścić rectangle$color <- color_constructor(new_color) aby zachować spójność. Chociaż możesz zmienić typ obiektu, nigdy nie powinieneś. Jak to ujął Hadley Wickham, R nie chroni cię przed sobą: możesz łatwo strzelić sobie w stopę. Dopóki nie wycelujesz pistoletu w stopę i nie pociągniesz za spust, nie będziesz miał problemu. Teraz pokażemy, jak można zastosować metodę set_color(). Wyświetlimy kolor prostokąta, spróbujemy zmienić go na czarny i wydrukujemy ponownie. Jak widać, zmiana nie została utrwalona w naszym obiekcie. Dzieje się tak, ponieważ R przekazuje obiekty przez wartość, a nie przez odniesienie. Oznacza to po prostu, że kiedy modyfikujemy prostokąt, tak naprawdę modyfikujemy kopię prostokąta, a nie prostokąta, który sami przekazaliśmy:

print(S3color(S3_rectangle))

#> [1] “blue”

#> attr (, class”)

#> [1] “S3Color”

set_color(S3_rectangle, „black”)

#> [1] “czarny prostokąt: 1 x 3 == 3”

print(S3color(S3_rectnagle))

#> [1] “blue”

#> attr (, “class”)

#> [1] “S3Color”

Czy zauważyłeś, że pod koniec funkcji set_color.S3Rectangle wróciliśmy do rectangle? W innych językach, które mogą nie być konieczne, ale w R robimy to, aby odzyskać zmodyfikowany obiekt. Aby utrzymać zmiany w naszym obiekcie, musimy faktycznie przypisać ten wynikowy obiekt do naszego własnegoS3_rectangle, a kiedy to zrobimy, możemy zobaczyć, że zmiana koloru została utrzymana. Ta właściwość nadaje S3 jego niezmienność. Jest to bardzo przydatne podczas pracy z programowaniem funkcjonalnym, ale może być nieco kłopotliwe podczas programowania zorientowanego obiektowo. Ta właściwość może powodować pewne mylące błędy, które przyzwyczają Cię do pracy w ten sposób

Metody publiczne i polimorfizm

Aby zdefiniować metodę dla klasy, musimy użyć funkcji UseMethod(), aby zdefiniować hierarchię funkcji. Poinstruuje R, aby szukał funkcji, której prefiks pasuje do bieżącej funkcji i sufiksu w kolejności od wektora nazw klas przekazywanego obiektu. Nazwy metod mają dwie części oddzielone znakiem „􀀐”, gdzie przedrostek to nazwa funkcji, a przyrostek to nazwa klasy. Jak widać, funkcje ogólne S3 działają na podstawie konwencji nazewnictwa, a nie jawnej rejestracji metod dla różnych klas. Zaczynamy od stworzenia metody S3area dla klasy S3Rectangle, a robimy to tworząc funkcję o nazwie S3area.S3Rectangle. Funkcja UseMethod() upewni się, że funkcja S3area.S3Rectangle otrzyma obiekt klasy S4rectangle, więc wewnątrz takiej funkcji możemy skorzystać z elementów wewnętrznych klasy. W tym przypadku weźmiemy długości a i b i pomnożymy je razem:

S3area.S3Rectangle  <- function(rectangle) {

return(rectnagle$a * rectnagle$b)

}

Zauważ, że możemy uzyskać dostęp do takich obiektów w obiekcie rectangle za pomocą operatora $. Nie jest to ograniczone do wykonania w ramach metody, więc tak naprawdę każdy obiekt może zmienić elementy wewnętrzne obiektu S3, ale tylko dlatego, że możesz, nie oznacza, że ​​powinieneś.

Teraz wywołamy metodę S3area ,t ak, jakby to było normalne wywołanie funkcji, do którego przekażemy utworzony wcześniej prostokąt prostokątny i powinniśmy zobaczyć, jak obszar jest drukowany do konsoli:

S23area(s3_rectangle)

#> Błąd w S3area (S3_rectangle): nie można znaleźć funkcji „S3area”

Co się stało? Błąd? W jaki sposób R może stwierdzić, że wywołanie funkcji S3area powinno faktycznie wyzwolić wywołanie metody S3area.S3Rectangle? Aby tak się stało, musimy zarejestrować nazwę za pomocą R i robimy to, wywołując funkcję definiującą, która w rzeczywistości sama używa nazwy S3area. Ta funkcja S3area otrzymuje obiekt dowolnego typu, niekoniecznie a, i używa funkcji to, aby powiedzieć, że powinna poszukaj wywołania metody „S3area”dla tego obiektu. W tym przypadku wiemy, że zostanie znaleziony tylko dla klasy S3Rectangle:

S3area <- functin(object) {

USeMethod(„S3area”)

}

Teraz możemy wywołać metodę S3area tak jak poprzednio, ale w tym przypadku uzyskamy rzeczywisty obszar. Oto jak zwykle tworzysz metody za pomocą S3:

S3area(S3_rectangle)

#> [1] 6

Teraz utworzymy metodę S3Color, aby zwrócić obiekt koloru dla prostokąta. Ponieważ obiekt koloru jest tylko typem znaków, nie musimy nic więcej zrobić, aby jakoś przeanalizować ten obiekt, jeśli chcemy tylko znaków:

S3color.S3Rectnagle <- function(rectangle) {

return(rectnagle$color)

}

S3color <- function(object) {

UseMethod(„S3color”)

}

Teraz wyświetlamy prostokąt. Jak widać, wywołanie print()po prostu pokazuje nam wnętrze obiektu i obiektów w nim zawartych:

print(S3_rectangle)

#> $ a

#> [1] 2

#>

#> $ b

#> [1] 3

#>

#> $ kolor

#> [1] “niebieski”

#> attr (, “klasa”)

#> [1] “S3Color”

#>

#> attr (, “klasa”)

#> [1] “S3Rectangle”

Możemy chcieć przeciążać tę funkcję, aby zapewnić inne wyjście. Aby to zrobić, tworzymy print.S3Rectnagle() i po prostu wyświetlamy ciąg, który powie nam kolor prostokąta, fakt, że jest to prostokąt, długość każdy z jego boków, a następnie jego obszar. Zauważ , że zarówno kolor, jak i obszar są pobierane przy użyciu metod, które zdefiniowaliśmy wcześniej, S3Color i S3areAa():

print.S3Rectnagle               <- function(rectnagle) {

print(paste(

S3olor(rectangle), „rectangle:”,

rectnagle$a, „x”, rectangle$b , „==”, S3area(rectangle)

))

}

A teraz, co powinno się stać, gdybyśmy po prostu wywołali funkcję print(), tak jak wcześniej z funkcją S3area()? Powinniśmy otrzymać błąd, prawda? Spójrzmy na następujący kod:

print(S3_rectangle)

#> [1] “niebieski prostokąt: 2 x 3 == 6”

Jak widać, nie mamy. W tym przypadku otrzymamy wynik, na który mieliśmy nadzieję. Powodem jest to, że funkcja print() w R jest funkcją S3, która została już zarejestrowana z funkcją UseMethod(). Oznacza to, że nasza definicja print.S3Rectangle nie musi być ponownie rejestrowana i możemy po prostu jej użyć. To całkiem fajne, prawda? To jedna z największych zalet stosowania polimorfizmu parametrycznego. Możemy rejestrować funkcje jako wywołania metod, których możemy, ale nie muszą, używać w przyszłości w nieoczekiwany sposób, ale nadal zapewniają one jednorodny interfejs dla użytkownika.

Klasy, konstruktory i kompozycja

Ideą obiektu jest tak naprawdę połączenie danych i odpowiednich metod. Listy w języku R są dobrze przystosowane do implementacji tego, ponieważ mogą zawierać różne typy danych, nawet funkcje, które są obiektami pierwszej klasy, które można przypisać lub zwrócić jak każdy inny. Faktycznie, możemy dosłownie tworzyć obiekty nowej klasy w R, pobierając listę i po prostu ustawiając atrybut klasy listy na nową wartość, tak jak tworzymy klasy w S3. Zamiast podawać definicje klas S3, udostępniamy konstruktory. Te konstruktory są odpowiedzialne za tworzenie obiektów (ciąg znaków, który ma parametr przekazany w przypadku S3Color i listę w przypadku S3Rectangle) i przypisanie ciągu do ich atrybutów klasowych. Te obiekty są następnie zwracane i reprezentują klasy, których będziemy używać. W przypadku prostokąta nasz konstruktor otrzymuje długość, prostopadłe boki oraz nazwę jego koloru. Konstruktor koloru otrzymuje tylko nazwę koloru:

color_constructor <- function(color){

class(color) <- „S3Color”

return(color)

}

retangle_constructor <- function(a,b,color) {

rectangle <- list(a = a, b = b, color = color_constructor(color))

class(rectangle)  <- „S3Rectangle”

return(rectangle)

}

Jak widać, zamiast przypisywać ciąg color, który jest przekazywany jako parametr do funkcji rectnagle_constructor() bezpośrednio w   color  elementu listy rectangle używamy funkcji color_constructor(), aby zapewnić klasę  Color,  a nie tylko ciąg znaków. Powinieneś to zrobić, jeśli dodasz zachowanie do abstrakcji kolorów, tak jak my. Teraz możemy utworzyć ,S3_rectangle wywołując rectnagle_constructor() i możemy wyświetlić tą klasę,  którą jest S3Rectangle, tak jak oczekiwaliśmy. Ponadto, jeśli wyświetlisz strukturę S3_rectnagle, zobaczysz, że zawiera ona dwie strony definicji prostokąta, klasę koloru i nazwy klas atrybutów:

S3_rectangle <- rectnagle_constructor(2,3,”blue”)

class(S3_rectangle)

#> [1] “S3Rectangle”

str(S3_rectangle)

#> Lista 3

#> $ a: num 2

#> $ b: num 3

#> $ color: Class ‘S3Color’ chr “blue”

#> – attr (*, “class”) = chr “S3Rectangle”

Czasami zobaczysz, że dodaliśmy przedrostek do obiektu z nazwą modelu obiektów, którego używamy (w tym przypadku S3). Na przykład S3Color i S3Rectangle. Kiedy to widzisz, oznacza to, że dana nazwa koliduje z odpowiednim obiektem w innym modelu obiektowym i musimy je rozróżnić. Jeśli tego nie zrobisz, możesz napotkać dość zagmatwane i trudne do zdiagnozowania błędy.

Model obiektowy S3

Jak być może pamiętasz, język R wywodzi się z języka S. Model obiektowy S ewoluował w czasie, a jego trzecia wersja wprowadziła atrybuty klas, co pozwoliło na model obiektowy S3, który znajdujemy dzisiaj w R. Wciąż jest to model obiektowy w R, a większość wbudowanych klas R jest typu S3. To poprawny i bardzo elastyczny model obiektowy, ale bardzo różni się od tego, do czego przyzwyczajeni są ludzie wywodzący się z innych języków obiektowych. S3 jest najmniej formalnym modelem obiektowym, więc brakuje mu kilku kluczowych aspektów. Na przykład S3 nie oferuje formalnych definicji klas, co oznacza, że ​​nie ma formalnej koncepcji dziedziczenia lub hermetyzacji, a polimorfizm jest uzyskiwany za pomocą typów ogólnych. Oczywiste jest, że jego funkcjonalność jest ograniczona pod pewnymi kluczowymi względami, ale programista ma dość dużą elastyczność. Jednak, jak ujął to Hadley Wickham w Advanced R, autorstwa Chapman and Hall, 2014: „S3 ma pewną elegancję w swoim minimalizmie: nie można zabrać jej żadnej części i nadal mieć użyteczny system obiektowy”