Genelde, Lua'daki her değer oldukça tahmin edilebilir işlem kümesine sahiptir. Sayıları toplayabilir, stringleri birleştirebilir, tablolara anahtar-değer çiftlerini ekleyebiliriz vb. Ancak tabloları toplayamaz, fonksiyonları karşılaştıramaz ve bir stringe çağrı yapamayız.
Metatablolar, tanımsız bir işlemle karşı karşıya kalındığında bir değerin davranışını değiştirmemize imkan verir. Örneğin metatabloları kullanarak, a ve b tablolar olmak üzere, a+b ifadesini Luan'nın nasıl hesaplayacağını tanımlayabiliriz. Lua iki tabloyu toplamaya giriştiğinde onlardan birinin bir metatabloya sahip olup olmadığını ve bu metatablonun bir __add alanına sahip olup olmadığını kontrol eder. Lua bu alanı bulursa toplamı hesaplayacak bir fonksiyon olan metametot adı verilen ilgili değeri çağırır.
Lua 'daki her değer metatabloya sahip olabilir. Tablolar ve userdata kendi özel metatablolarına sahiptir; diğer türlerin değerleri bu türün tüm değerleri için tek bir metatabloyu paylaşır (Lua 5.0'da sadece tablolar ve userdata metatablolara sahip olabiliyordu. Ekseriye zaten metatablolar ile kontrol etmek istediğimiz türler bunlar). Lua yeni tabloları daima metatabloları olmadan oluşturur:
t = {}
print(getmetatable(t)) --> nil
Herhangi bir tabloya metatablo eklemek veya değiştirmek için setmetatable'ı kullanabiliriz:
t1 = {} setmetatable(t, t1) assert(getmetatable(t) == t1)
Herhangi bir tablo herhangi bir değerin metatablosu olabilir; ilgili tablolar grubu, ortak davranışları betimleyen ortak bir metatablo paylaşabilir; bir tablo kendi özgün davranışını betimleyen kendi metatablosuna da sahip olabilir. Herhangi yapılandırma geçerlidir.
Lua 'dan sadece tabloların metatablolarını set edebiliriz; Diğer türdeki değerlerin metatablolarını manipüle etmek için C kodu kullanmamız gerekir. (Bu kısıtlamanın ana nedeni, tip bazında metatablolaların aşırı kullanımını engellemektir. Lua'nın eski sürümlerindeki deneyimler sıklıkla, yeniden kullanılabilir olmayan kodlamalara yol açtığını göstermiştir.) Daha sonra göreceğimiz üzere, bölüm 20'de string kütüphanesi stringler için metatablolar set eder. Diğer tüm tipler varsayılanda metatabloya sahip değildir:
print(getmetatable("hi")) --> table: 0x80772e0 print(getmetatable(10)) --> nil
13.1 Aritmetik Metametotlar
Bu bölümde metatabloların nasıl kullanılacağını açıklamak için basit bir örnek sunacağız. İki kümenin birleşimini, kesişimini hesaplayacak fonksiyonlarla, kümeleri temsil etmek için tabloları kullandığımızı varsayalım. isim alanımızı temiz tutmak için bu fonksiyonları Set adlı bir tablo içinde depoluyoruz:
Set = {} -- verilen listenin değerleriyle yeni bir küme yarat function Set.new(l) local set = {} for _, v in ipairs(l) do set[v] = true end return set end function Set.union(a, b) local res = Set.new {} for k in pairs(a) do res[k] = true end for k in pairs(b) do res[k] = true end return res end function Set.intersection(a, b) local res = Set.new {} for k in pairs(a) do res[k] = b[k] end return res end
Örneklerimizi kontrol etmeye yardımcı olması için kümeleri yazdıracak bir fonksiyon tanımlıyoruz aynı zamanda:
function Set.tostring(set) local l = {} -- kümeden tüm elemanların konulacağı liste for e in pairs(set) do l[#l + 1] = e end return "{" .. table.concat(l, ", ") .. "}" end function Set.print(s) print(Set.tostring(s)) end
Şimdi, toplama operatörünün ('+') iki kümenin birleşimini hesaplamasını istiyoruz. Bunun için, toplama operatörüne nasıl reaksiyon gösterileceğini tanımlayacak bir metatabloyu paylaşacak kümeleri temsil eden tüm tabloları ayarlayacağız. İlk adımımız, kümeler için metatablo olarak kullanacağımız klasik bir tablo oluşturmak:
local mt = {} -- kümeler için metatablo
Bir sonraki adım, kümeleri oluşturan Set.new fonksiyonunu modifiye etmek. Yeni sürüm, yalnızca bir ekstra satıra sahip; oluşturulan tablolar için metatablo olarak mt'nin set edilmesi:
function Set.new(l) -- 2. sürüm local set = {} setmetatable(set, mt) for _, v in ipairs(l) do set[v] = true end return set end
Bundan sonra, Set.new ile oluşturduğumuz her küme metatablosu olarak aynı tabloya sahip olacak:
s1 = Set.new {10, 20, 30, 50} s2 = Set.new {30, 1} print(getmetatable(s1)) --> table: 00672B60 print(getmetatable(s2)) --> table: 00672B60
Son olarak, metatabloya, toplamanın nasıl gerçekleşeceğini tanımlayan metametotu, __add alanını ekliyoruz:
mt.__add = Set.union
Bundan sonra Lua iki kümeyi toplamaya giriştiğinde, argümanları olarak iki operatörle set.union fonksiyonunu çağırır.
Devamdaki gibi metametot ile, küme birleşimi yapmak için '+' operatörünü kullanabilirsiniz:
s3 = s1 + s2
Set.print(s3) --> {1, 10, 20, 30, 50}
Benzer şekilde, küme kesişimini gerçekleştirmek için '*' işlecini-operatörünü ayarlayabiliriz:
mt.__mul = Set.intersection
Set.print((s1 + s2)*s1) --> {10, 20, 30, 50}
Her aritmetik operatör için, metatabloda karşılık gelen bir alan adı vardır. __add ve __mul haricindekiler __sub (çıkarma için), __div (bölme için), __unm (negatifleştirme için), __mod (mod için), ve __pow (üs için). birleştirme işleci(..) için davranış tanımlamak için __concat alanınıda tanımlayabiliriz.
İki kümeyi topladığımızda hangi metatablonun kullanılacağı hakkında bir şüphe yok. Bununla birlikte örneğin farklı metatablolulu iki değeri miks eden bir ifade yazabiliriz:
s = Set.new{1,2,3} s = s + 8
Lua bir metametota bakarken devamdaki adımları gerçekleştirir: Eğer ilk değer __add alanlı bir metatabloya sahipse, ikinci değerden bağımsız olarak , Lua metametot olarak bu alanı kullanır; aksi durumda, ikinci değer __add alanlı metatabloya sahipse, Lua metametot olarak bu alanı kullanır; aksi halde, Lua hata yükseltir. Bu nedenle son örnek 10+s ve "hello"+s ifadeleri olacak şekilde Set.union 'ı çağırır.
Lua bu miks türler hakkında endişe taşımaz ancak uygulamamız taşır. s=s+8 örneğini çalıştırırsak, aldığımız hata Set.union içinde olacaktır:
bad argument #1 to 'pairs' (table expected, got number)
Daha net hata iletileri istiyorsak, işlemi gerçekleştirmeye girişmeden önce işleç türlerini açıkça kontrol etmeliyiz:
function Set.union (a, b) if getmetatable(a) ~= mt or getmetatable(b) ~= mt then error("attempt to ’add’ a set with a non-set value", 2) end <önceki gibi>
error 'e ikinci argüman (bu örnekte 2) hata mesajını işlemin çağrıldığı yere yönlendirdiğini hatırlayın.
13.2 İlişkisel Metametotlar
Metatablolar ayrıca, __eq (eşit), __lt (küçük) ve __le (eşit veya küçük) metametotlarıyla, ilişkisel operatörlere anlam yüklememize imkan verir. Devamdaki üç ilişkisel operatör için ayrı metametotlar yoktur, Lua 'nın çevrimi ile a~=b not (a==b) 'e , a>b b<a 'e ve a>=b b<=a 'e.
Lua 4.0'a kadar tüm sıralama operatörleri teke çevirilirdi, a<=b not (b<a)'e gibi. Bununla birlikte, kısmi sıralamaya sahip olduğunda bu çeviri yanlıştır, yani tipimizdeki tüm öğeler tam bir şekilde sıralanmadığında. Örneğin, sayı olmayan bir değer (NaN) sebebiyle kayan nokta sayıları çoğu makinede tam olarak sıralanmaz. IEEE 754 standardına göre, şu anda hemen hemen tüm kayan nokta donanımları tarafından kabul edilen NaN tanımsız bir değeri temsil eder , 0/0 sonucu gibi. Standart, herhangi bir karşılaştırmada NaN'ın false olarak sonuçlanması gerektiğini belirtir. Bu, NaN<=x 'nin daima false olduğu anlamına gelir, tabi x<NaN 'da false'dir. Ayrıca, a<= b 'den not (b<a) çeviri bu durumda geçerli değildir.
kümeler örneğimizde benzer bir sorunumuz var. kümelerde <= anlamı, küme kapsamadır: a<=b, a b'nin bir alt kümesi olduğu anlamına gelir. Bu anlamla, hem a<=b hem de b<a'nın false olması mümkündür; bu nedenle, __le (eşit veya küçük) ve __lt(küçük) için ayrı uyarlamalara ihtiyacımız var:
mt.__le = function(a, b) -- küme kapsam for k in pairs(a) do if not b[k] then return false end end return true end mt.__lt = function(a, b) return a <= b and not (b <= a) end
Nihayetinde, küme kapsama ile eşit kümeyi tanımlayabilirsiniz:
mt.__eq = function(a, b) return a <= b and b <= a end
Bu tanımlamalardan sonra kümeleri karşılaştırmaya hazırız:
s1 = Set.new {2, 4} s2 = Set.new {4, 10, 2} print(s1 <= s2) --> true print(s1 < s2) --> true print(s1 >= s1) --> true print(s1 > s1) --> false print(s1 == s2 * s1)--> true
Aritmetik metametotların aksine, ilişkisel metametotlar miks tiplere uygulanamaz. Miks tipler için davranışlar, Lua'daki bu operatörlerin için olan ortak davranışları taklit eder. sıralama için bir stringi bir sayı ile karşılaştırmaya çalışırsanız, Lua bir hata yükseltir. Benzer şekilde, sıralamak için farklı metametotlu iki nesneyi karşılaştırmaya çalışırsanız Lua bir hata yükseltir.
Eşitlik karşılaştırması asla bir hata yükseltmez ancak iki nesne farklı metametotlara sahipse eşitlik işlemi false ile sonuçlanır, herhangi bir metametot çağırmadan bile. Yine bu davranış, değerleri ne olursa olsun stringleri sayılardan farklı olarak sınıflandıran Lua'nın ortak davranışını taklit eder.
Lua, eşitlik metametotunu yalnızca karşılaştırılan iki nesne metametotu paylaştığında çağırır.
13.3 Kütüphane-Tanımlı Metametotlar
Metatablolarda kendi alanlarını tanımlama, kütüphaneler için yaygın bir uygulamadır. Şimdiye kadar gördüğümüz tüm metametotlar Lua çekirdeği için olanlar. Bir işlemde yer alan değerlerin metatabloları olduğu ve bu metatabloların o işlem için metametotlar tanımlandığını tespit eden sanal makinedir. tabi, metatablolar klasik tablolar olduğundan, herkes onları kullanabilir.
tostring fonksiyonu tipik bir örnek sağlar. Daha önce gördüğümüz üzere, tostring tabloları oldukça basit bir formatta temsil eder:
print({}) --> table: 0x8062ac0
(print fonksiyonu her zaman çıktısını biçimlendirmek için tostring'i çağırır.). Ancak, herhangi bir değeri biçimlendirirken tostring önce değerin __tostring metametodunun olup olmadığını denetler.
Bu durumda, tostring işi yapmak için bir argüman olarak nesnenin geçildiği metametodu çağırır.
Bu metametot 'un döndürdüğü şey tostring'in sonucudur.
Kümeler örneğimizde, kümeyi bir string olarak sunmak için hazırda bir fonksiyon tanımladık. Bu nedenle, metatablo'da sadece __tostring alanını ayarlamaya ihtiyaç var:
mt.__tostring = Set.tostring
Bundan sonra, argüman olarak kümeyle print'i çağırdığımızda, print , Set.tostring'i çağıran tostring'i çağırır:
s1 = Set.new {10, 4, 5}
print(s1) --> {4, 5, 10}
setmetatable ve getmetatable fonksiyonları ayrıca bir metaalan kullanır , bu durumda metatabloları korumak için. Kümelerinizi korumak istediğinizi varsayalım, bu şekilde kullanıcılar metatabloları ne görebilir ne de değiştirebilir. metatabloya __metatable alanı set ederseniz, getmetatable bu alanın değerini döndürür, buna karşılık setmetatable bir hata yükseltir:
mt.__metatable = "senin işin değil" s1 = Set.new{} print(getmetatable(s1)) --> senin işin değil setmetatable(s1, {}) stdin:1: cannot change protected metatable
13.4 Tablo-Erişim Metametotları
Aritmetik ve ilişkisel operatörler için olan metametotlar farklı durumlar için davranışlar tanımlar. Dilin normal davranışını değiştirmezler. Ancak Lua, iki normal durum için tabloların davranışını değiştirmek için bir yol sunar, bir tabloda bulunmayan alanları sorgu ve modifikasyonu.
__index metametodu
Daha önce, bir tabloda eksik bir alana eriştiğimizde sonucun nil olduğunu söyledim. Bu doğru, ama tamamen gerçek değil. Aslında, bu tür erişimler __index metametodunu aramasını için yorumlayıcıyı tetikler: böyle bir metot yoksa, genelde olduğu gibi, erişim nil olarak sonuçlanır; aksi takdirde metametot sonucu sağlar.
Burada klasik örnek kalıtımdır. pencerelerimi-kontrollerimizi betimleyen çeşitli tablolar oluşturmak istediğimizi varsayalım. Her tablo, konum, boyut, renk ve benzeri çeşitli pencere-kontrol parametrelerini tanımlamalıdır. Tüm bu parametrelerin varsayılan değerleri vardır ve bu şekilde sadece varsayılanı olmayan parametreleri vererek pencere-kontrol nesneleri yaratmak istiyoruz diyelim. İlk alternatif, olmayan alanları dolduran bir oluşturucu sağlamaktır. İkinci alternatif, yeni pencerelerin bir prototip penceresinden tüm eksik alanlar için miras almasını sağlamak. İlk olarak, bir metatabloyu paylaşan yeni pencereleri oluşturacak prototip ve constructor-yapıcı fonksiyonu deklare ederiz:
Window = {} -- namespace-isim alanı oluştur -- varsayılan değerli prototip oluştur Window.prototype = {x = 0, y = 0, width = 100, height = 100} Window.mt = {} -- create a metatable -- constructor-yapıcı fonksiyonu deklare et function Window.new(o) setmetatable(o, Window.mt) return o end
Şimdi, __index metametodunu tanımlıyoruz:
Window.mt.__index = function(table, key) return Window.prototype[key] end
Bu koddan sonra, yeni bir pencere oluşturur ve sunulmamış alana sahip olup olmadığını sorguluyoruz:
w = Window.new {x = 10, y = 20}
print(w.width) --> 100
Lua, w'nin istenen alana sahip olmadığını ancak bir __index alanına sahip bir metatablo olduğunu algıladığında, w (tablo) ve “width” (eksik anahtar) argümanlarıyla bu __index metametodunu çağırır. Metametot, prototipi verilen anahtarla indeksler ve sonucu döndürür.
miras için __index metametodunun kullanımı, çok yaygın bir Lua kısayolu sağlar . İsmine rağmen, __index metametodunun bir fonksiyon olması gerekmez: bunun yerine bir tablo olabilir. Bir fonksiyon olduğunda, Lua, daha yeni gördüğümüz üzere argümanları olarak olmayan anahtar ve tablo ile onu çağırır. O bir tablo olduğunda, Lua bu tablodaki erişimi yineler. Bu şekilde önceki örneğimizde, __index'i basitçe şu şekilde deklare edebiliriz:
Window.mt.__index = Window.prototype
Artık Lua, metatablo'nun __index alanını baktığında bir tablo olan Window.prototip değerini bulur.
Sonuç olarak Lua bu tablodaki erişimi tekrarlar yani aşağıdaki kodun eşdeğerini yürütür:
Window.prototype["width"]
Bu erişim sonra istenen sonucu verir.
__index metametotu olarak bir tablonun kullanımı tekli miras uygulamasının hızlı ve basit yolunu sağlar. fonksiyon, daha masraflı olmasına rağmen, daha fazla esneklik sağlar: çoklu miras, önbellekleme(caching) ve diğer birçok varyasyon uygulayabiliriz. Bu miras biçimlerini 16. bölümde ele alacağız.
Bir tabloya __index metametodunu çağırmadan erişmek istediğimizde rawget fonksiyonunu kullanırız. rawget(t, i) çağrısı, tablo t'e ham bir erişim yapar, yani metatabloları dikkate almadan ilkel-temel bir erişim. Ham erişim yapmak kodunuzu hızlandırmaz ( fonksiyon çağrısının yükü, sahip olabileceğiniz herhangi bir kazancı öldürür) ancak daha sonra göreceğimiz üzere bazen buna ihtiyacınız vardır.
__newindex metametodu
__newindex metametodu, tablo erişimleri için __index'in yaptığını, tablo güncellemeleri için yapar.
Bir tabloda olmayan bir indeks için bir değer atadığınızda, yorumlayıcı __newindex metametodunu arar: varsa, yorumlayıcı atama yapmak yerine onu çağırır. __index 'deki gibi, metametot bir tablo ise, yorumlayıcı orjinali yerine bu tabloya atama yapar. Yine, metametodu es geçmenize izin veren bir raw fonksiyonu vardır: rawset(t,k, v) çağrısı herhangi bir metametodu çağırmadan tablo t'deki anahtar k ile ilişkilendirilmiş v değerini set eder.
__index ve __newindex metametodlarının kombine kullanımı, salt okunur tablolar, varsayılan değerleri olan tablolar ve nesne yönelimli programlama için miras gibi Lua'da birkaç güçlü yapıları sağlar. Bu bölümde bu kullanımlardan bazılarını göreceğiz. Nesne yönelimli programlamanın kendi bölümü var.
Varsayılan değerli tablolar
Normal bir tablodaki herhangi bir alanın varsayılan değeri nil'dir. Bu varsayılan değeri metatablolar ile değiştirmek kolaydır:
function setDefault(t, d) local mt = {__index = function() return d end} setmetatable(t, mt) end tab = {x = 10, y = 20} print(tab.x, tab.z) --> 10 nil setDefault(tab, 0) print(tab.x, tab.z) --> 10 0
setDefault çağrısından sonra tab'daki boş alana herhangi bir erişim sıfır döndüren (bu metametot için d'nin değeri) __index metametodunu çağırır.
setDefault fonksiyonu varsayılan değere ihtiyaç duyan her tablo için yeni bir metatablo oluşturur.
Varsayılan değerlere ihtiyaç duyan birçok tablomuz varsa bu masraflı olabilir. Bununla birlikte, metatablo, metametoduna bağlı d varsayılan değerine sahip bu nedenle fonksiyon tüm tablolar için tek bir metatablo kullanamaz. Farklı varsayılan değerlere sahip tablolar için tek bir metatablo kullanımına izin vermek için özel bir alan kullanarak tablonun kendisinde her tablonun varsayılan değerini saklayabiliriz. İsim çakışmaları konusunda endişelenmiyorsak, özel alanımız için “___” gibi bir anahtar kullanabiliriz:
local mt = {__index = function(t) return t.___ end} function setDefault(t, d) t.___ = d setmetatable(t, mt) end
İsim çakışmaları konusunda endişeleniyorsak bu özel anahtarın benzersizliğini sağlamak kolay.
Tek ihtiyacımız olan yeni bir tablo oluşturmak ve anahtar olarak onu kullanmaktır:
local key = {} -- benzersiz anahtar local mt = {__index = function(t) return t[key] end} function setDefault(t, d) t[key] = d setmetatable(t, mt) end
Her tabloyu varsayılan değeriyle ilişkilendirmek için alternatif bir yaklaşım, indekslerin tablolar olduğu ve değerlerin varsayılan değerleri olduğu ayrı bir tablo kullanmaktır. Bununla birlikte, bu yaklaşımın doğru uygulanması için zayıf tablolar denilen özel bir tablo cinsine ihtiyacımız var
ve bu yüzden burada kullanmayacağız; konuya 17. bölümde geri döneceğiz.
Başka bir alternatif, aynı varsayılanlarla tablolar için aynı metatabloyu yeniden kullanmak için metatabloların memoizasyonudur(Memoization). Bununla birlikte, bu da zayıf tablolara ihtiyaç duyuyor, böylece yine 17. bölüme kadar beklemek zorunda kalacağız.
Tablo Erişimlerini İzleme
Hem __index hem de __newindex yalnızca tabloda indeks bulunmadığında geçerlidir. Bir tabloya olan tüm erişimleri yakalamak için tek yol onu boş tutmaktır. Yani, bir tabloya tüm erişimleri izlemek istiyorsak, gerçek tablo için bir vekil(proxy) oluşturmalıyız. Bu proxy, tüm erişimleri izleyen ve bunları orijinal tabloya yönlendiren __index ve __newindex metametotlu boş bir tablodur. t'i izlemek istediğimiz orijinal tablo olduğunu varsayalım. Şöyle bir şey yazabiliriz:
t = {} -- orjinal tablo (herhangi yerde yaratıldı) -- orijinal tabloya özel erişim sağlama local _t = t -- proxy yarat t = {} -- metatablo yarat local mt = { __index = function(t, k) print("*access to element " .. tostring(k)) return _t[k] -- orjinal tabloya eriş end, __newindex = function(t, k, v) print("*update of element " .. tostring(k) .. " to " .. tostring(v)) _t[k] = v -- orjinal tabloyu güncelle end } setmetatable(t, mt)
Bu kod, t'e olan tüm erişimleri izler:
> t[2] = "hello"
*update of element 2 to hello
> print(t[2])
*access to element 2
hello
(Maalesef, bu düzenin tabloları dolaşmamıza imkan vermediğine dikkat edin. pairs fonksiyonu proxy üzerinde çalışacaktır, orijinal tablo değil.)
Birkaç tabloyu izlemek istersek her biri için farklı bir metatabloya gerek yok. Bunun yerine, her bir proxy'i bir şekilde orjinal tablosuna ilişkilendirebilir ve tüm proxy'ler için ortak bir metatablo paylaşabiliriz. Bu sorun, önceki bölümde tartıştığımız varsayılan değerlerine tabloları ilişkilendirme sorununa benzer. Örneğin, özel bir anahtar kullanarak orijinal tabloyu bir proxy alanında tutabiliriz.
Sonuç aşağıdaki koddur:
local index = {} -- özel indeks yarat local mt = { -- metatablo yarat __index = function(t, k) print("*access to element " .. tostring(k)) return t[index][k] -- orjinal tabloya eriş end, __newindex = function(t, k, v) print("*update of element " .. tostring(k) .. " to " .. tostring(v)) t[index][k] = v -- orjinal tabloyu güncelle end } function track(t) local proxy = {} proxy[index] = t setmetatable(proxy, mt) return proxy end
Artık tablo t'i izlemek istediğimizde tek yapmamız gereken t=track(t) 'i çalıştırmaktır.
Salt okunur tablolar
Salt okunur tabloları uygulamak için vekil-proxy konseptini adapte etmek kolay. Tek yapmamız gereken, tabloyu güncelleme girişimini takip ederken bir hata yükseltmek. __index metametodu için, , fonksiyon yerine, tablo kullanabiliriz;orijinal tablonun kendisini. sorguları izlemenize gerek yok; tüm sorguları orjinal tabloya yönlendirmek daha basit ve daha etkili. Bununla birlikte, bu kullanım, her bir salt okunur proxy için yeni bir metatablo gerektirir, __index orjinal tabloyu gösterecek şekilde:
function readOnly(t) local proxy = {} local mt = { -- metatablo oluştur __index = t, __newindex = function(t, k, v) error("salt-okunur tabloyu güncelleme girişimi", 2) end } setmetatable(proxy, mt) return proxy end
Kullanım örneği olarak, haftanın günleri için salt okunur bir tablo oluşturabiliriz:
gunler = saltOkunur{"Pazar", "Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi"}
print(gunler[1]) --> Pazar gunler[2] = "Herhangi" stdin:1: salt-okunur tabloyu güncelleme girişimi
Hiç yorum yok:
Yorum Gönder