C # 'da "dahili" anahtar kelime için pratik kullanımlar


420

internalC # 'daki anahtar kelime için pratik kullanımın ne olduğunu açıklar mısınız?

internalDeğiştiricinin mevcut montaja erişimi sınırladığını biliyorum , ancak ne zaman ve hangi durumda kullanmalıyım?

Yanıtlar:


379

Aynı derlemedeki diğer birçok sınıftan erişmek istediğiniz, ancak diğer derlemelerde kodun erişemediğinden emin olmak istediğiniz yardımcı program veya yardımcı sınıflar / yöntemler.

Gönderen MSDN (archive.org yoluyla):

Dahili erişimin yaygın kullanımı bileşen tabanlı geliştirmedir, çünkü bir grup bileşenin uygulama kodunun geri kalanına maruz kalmadan özel bir şekilde işbirliği yapmasını sağlar. Örneğin, grafiksel kullanıcı arabirimleri oluşturmak için bir çerçeve, iç erişime sahip üyeler kullanarak işbirliği yapan Denetim ve Form sınıfları sağlayabilir. Bu üyeler dahili olduğundan, çerçeveyi kullanan koda maruz kalmazlar.

InternalsVisibleToHedef derleme iç sınıflarına özel erişim izni verilen "arkadaş" derlemeleri oluşturmak için derleme düzeyi özniteliği ile birlikte iç değiştiriciyi de kullanabilirsiniz .

Bu, daha sonra test edilecek düzeneğin dahili üyelerini çağırmasına izin verilen birim test düzeneklerinin oluşturulması için yararlı olabilir. Tabii ki başka montajlara bu erişim seviyesi verilmiyor, bu nedenle sisteminizi serbest bıraktığınızda kapsülleme korunuyor.


8
InternalsVisibleTo özniteliği çok yardımcı olur. Teşekkür ederim.
erict

33
Duygularım, içsel ihtiyaç duyduğunuz andan itibaren, bir yerde tasarımın yanlış bir şey olduğu. IMHO, kişi tüm sorunları çözmek için saf OO kullanabilmelidir. Şu anda, .NET Framework kaynağında dahili olarak yaklaşık bir milyonuncu kullanıma bakıp, kullandıkları şeylerden faydalanma yolumu kısıtlıyorum. Dahili kullanımı sayesinde, yüzeysel olarak modüler bir monolit yarattılar, ancak şeyleri izole etmeye çalıştığınızda yıkılıyorlar. Ayrıca, kurtarmaya yansıma olduğu için güvenlik de bir tartışma değildir.
Umutsuzluğun Yüz burcu

26
@ GrimaceofDespair: Buraya tanılayıcı bir yorum ekleyeyim. Ben şahsen büyük çoklu proje çözümlerinde "genel" değiştiricinin çok kullanışlı bir sürümü olarak görüyorum. Bu değiştiriciyi alt projemdeki bir sınıfa eklemek, bu sınıfın çözümdeki diğer alt projeler tarafından "rasgele kullanımını" yasaklar ve kütüphanemin uygun kullanımını zorlamamı mümkün kılar. Daha sonra yeniden düzenleme yapmam gerekirse, kendime "bekle, ama neden o adam gitti ve Point sınıfının sadece bu özel hesaplama içinde kendi amaçlarım için ihtiyacım olan bir örneğini" yarattı?
KT.

3
Başka bir deyişle, herkes SmtpClient'in içlerine güveniyorsa ve bir anda orada bir hata keşfedildiyse, .NET'in bu hatayı düzeltmek, hatta belki de uzun süre tutmak gibi ciddi sorunları olurdu. Windows geliştiricilerinin, çeşitli dahili özellikleri ve hataları kullanan tüm eski programlarla "bugwards uyumluluğunu" korumak için ne kadar süre geçmesi gerektiğini açıklayan eğlenceli bir makaleyi okuduğumu hatırlıyorum. Bu hatayı .NET ile tekrar etmek anlamlı değildir.
KT.

13
Bence içsel yol kamuoyundan daha yararlı. Herkese açık olarak kullandığınız tek zaman, "Burada kullanıcı - kullanmanız için yararlı bir işlev sağlıyorum." Kullanıcı kitaplığı yerine bir uygulama yazıyorsanız, insanlara dışarıdan arama yapmaları için herhangi bir işlev vermeye çalışmadığınız için her şey dahili olmalıdır. Bir kullanıcı kitaplığı yazıyorsanız, yalnızca kullanıcıya vermeyi, bakımını yapmayı, desteklemeyi ve sonuna kadar saklamayı beklediğiniz yararlı işlevler herkese açık olmalıdır. Aksi takdirde, dahili yapın.
Darrell Plank

85

Bob'un BigImportantClass'a ihtiyacı varsa, Bob'un A projesine sahip olan kişilere, BigImportantClass'ın ihtiyaçlarını karşılamak için yazılacağını, ihtiyaçlarını karşıladığından emin olmak için test edileceğini, ihtiyaçlarını karşıladığını belgelediğini ve bir sürecin belgelendiğini garanti etmesi gerekir. artık ihtiyaçlarını karşılamayacak şekilde asla değiştirilmemesini sağlamak için uygulanacaktır.

Eğer bir sınıf içsel ise, bu süreçten geçmek zorunda değildir, bu da A Projesi için bütçeyi başka şeylere harcayabilecekleri tasarrufdan kurtarır.

İçsel nokta Bob için hayatı zorlaştırdığı değildir. Proje A'nın özellikler, kullanım ömrü, uyumluluk vb. Hakkında ne pahalı vaatlerde bulunduğunu kontrol etmenizi sağlar.


5
Mantıklı gelir. Sadece dahili üyeleri geçersiz kılabilmemizi diliyoruz, çünkü hepimiz yetişkinleri burada kabul ediyoruz.
cwallenpoole

6
@EricLippert Ne demek istediğim pek değil. İçinde "iç" olan derlenmiş kodu devralırsam, sıkıştım, değil mi? Derleyiciye "Hayır, diğer geliştiricinin size yalan söylediğini, bunun yerine bana güvenmelisiniz" demesinin bir yolu yoktur.
cwallenpoole

11
@cwallenpoole: Hoşunuza gitmeyen kod yazan yalancılar tarafından yazılan bir kod kullanıyorsanız, kodunu kullanmayı bırakın ve kendi kodunuzu daha iyi yazın.
Eric Lippert

2
@EricLippert Yanakta dil olması gerekiyordu, kırmak istememiştim. (Çoğunlukla böyle durumlarda SOL olduğumu doğrulamaya çalışıyordum).
cwallenpoole

5
@cwallenpoole: En azından rahatsız değilim. Kendini bu talihsiz duruma sıkışmış bulursan iyi tavsiyelerde bulunuyorum.
Eric Lippert

60

Internal'i kullanmanın bir başka nedeni, ikili dosyalarınızı gizlemenizdir. Obfuscator, herhangi bir iç sınıfın sınıf adını karıştırmanın güvenli olduğunu bilirken, ortak sınıfların adı karıştırılamaz, çünkü bu mevcut referansları kırabilir.


4
Pff. Bytecode'a bir sökücü ile bakmak, yine de kodun ne yaptığını oldukça netleştirecektir. Obfuscator'lar gerçekten iç kısımlara girmeye çalışan herkes için hafif bir caydırıcıdır. Faydalı fonksiyon isimleri almadığı için durmuş bir hacker'ı hiç duymadım.
Nyerguds

28
yani uyurken kapıyı kilitlemiyorsun, çünkü bir kilidin durduğu bir hırsızı hiç duymadın mı?
Sergio

4
Bu yüzden kaynak kodunda sınıf ve değişken isimlerini karıştırıyorum. Bir PumpStateComparator'ın ne olduğunu anlayabilirsiniz, ancak bir LpWj4mWE'nin ne olduğunu tahmin edebilir misiniz? #jobsecurity (Bunu yapmıyorum ve lütfen bunu yapmayın, internet insanlar.)
Slothario

32

Basit bir ortak API içine bir ton karmaşık işlevselliği içine alan bir DLL yazıyorsanız, halka açık olarak maruz kalmayacak sınıf üyelerinde "iç" kullanılır.

Karmaşıklığı (diğer bir deyişle kapsülleme) gizlemek, kaliteli yazılım mühendisliğinin ana konseptidir.


20

Yönetilmeyen kodun üzerine bir sarmalayıcı oluştururken dahili anahtar kelime çok kullanılır.

DllImport yapmak istediğiniz bir C / C ++ tabanlı kitaplığınız varsa, bu işlevleri bir sınıfın statik işlevleri olarak içe aktarabilir ve bunları içsel hale getirebilirsiniz, böylece kullanıcınız orijinal API'ya değil, yalnızca paketinize erişebilir. bir şeyle uğraşmak. Fonksiyonlar statiktir, ihtiyacınız olan çoklu sargı sınıfları için montajın her yerinde kullanabilirsiniz.

Mono.Cairo'ya bir göz atabilirsiniz, bu yaklaşımı kullanan cairo kütüphanesinin etrafındaki bir sarıcıdır.


12

"Mümkün olduğu kadar katı değiştirici olarak kullan" kuralı tarafından yönetilen, açıkça başka bir derlemeden erişmesi gerekene kadar başka bir sınıftan, başka bir sınıftan metoda erişmem gereken her yerde dahili kullanıyorum.

Montaj arayüzü genellikle sınıf arayüzlerinin toplamından daha dar olduğundan, kullandığım birçok yer var.


3
Eğer "olabildiğince katı değiştirici olarak kullanırsanız" neden özel değil?
R. Martinho Fernandes

6
Sınıf özelliklerine / yöntemlerine sınıfın dışında erişilmesi gerektiğinde özel çok katı olduğundan ve bunlara başka bir erişimden erişilmesi gerektiğinde, bly çok sık değildir.
Ilya Komakhin

11

Ben iç aşırı aşırı bulmak. belirli işlevsellikleri yalnızca diğer tüketicilere veremeyeceğiniz belirli sınıflara maruz bırakmamalısınız.

Bence bu arayüzü bozuyor, soyutlamayı bozuyor. Bu, asla kullanılmaması gerektiği anlamına gelmez, ancak daha iyi bir çözüm, farklı bir sınıfa yeniden bakmak veya mümkünse farklı bir şekilde kullanmaktır. Ancak, bu her zaman mümkün olmayabilir.

Sorunlara neden olmasının nedenleri, başka bir geliştiricinin sizinkiyle aynı derlemede başka bir sınıf oluşturmakla suçlanabilmesidir. İç kısımlara sahip olmak, soyutlamanın netliğini azaltır ve yanlış kullanıldığında sorunlara neden olabilir. Herkese açık hale getirmiş olmanızla aynı sorun olur. Diğer geliştirici tarafından inşa edilen diğer sınıf, tıpkı herhangi bir dış sınıf gibi, hala bir tüketicidir. Sınıf soyutlama ve kapsülleme sadece dış sınıflar için / sınıflardan koruma için değil, tüm sınıflar için de geçerlidir.

Başka bir sorun, birçok geliştiricinin montajda başka bir yerde kullanmaları gerektiğini düşünecek ve o zaman ihtiyaç duymasalar bile yine de dahili olarak işaretleyecekler. Başka bir geliştirici daha sonra almak için orada olduğunu düşünebilir. Tipik bir ihtiyaç olana kadar özel olarak işaretlemek istersiniz.

Ancak bunların bir kısmı öznel olabilir ve asla kullanılmaması gerektiğini söylemiyorum. Sadece gerektiğinde kullanın.


Sık sık, aynı derleme içindeki diğer etki alanı nesnelerine açık olması gereken üyeleri olan etki alanı nesneleri oluştururum. Ancak, aynı üyeler derleme dışında istemci kodu tarafından kullanılabilir OLMAMALIDIR. Bu gibi durumlar için 'dahili' çok uygundur.
Rock Anthony Johnson

8

Geçen gün, belki hafta hatırlayamadığım bir blogda ilginç bir tane gördüm. Temelde bunun için kredi alamıyorum ama yararlı bir uygulama olabilir düşündüm.

Diyelim ki soyut bir sınıfın başka bir meclis tarafından görülmesini istediniz, ama birisinin onu devralmasını istemiyorsunuz. Mühürlü işe yaramaz çünkü bir nedenden dolayı soyuttur, o derlemedeki diğer sınıflar ondan miras alırlar. Özel çalışmaz, çünkü diğer meclisin herhangi bir yerinde bir Ebeveyn sınıfı bildirmek isteyebilirsiniz.

ad alanı Base.Assembly
{
  kamu soyut sınıfı
  {
    dahili soyut boşluk SomeMethod ();
  }

  // Aynı montajda olduğu için bu iyi çalışıyor.
  kamu sınıfı ChildWithin: Ebeveyn
  {
    iç geçersiz kılma geçersizliği SomeMethod ()
    {
    }
  }
}

namespace Another.Assembly
{
  // Kaboom, çünkü dahili bir yöntemi geçersiz kılamazsınız
  kamu sınıfı ChildOutside: Ebeveyn
  {
  }

  genel sınıf testi 
  { 

    // Sadece iyi
    özel Veli _parent;

    herkese açık Test ()
    {
      //Hala iyi
      _parent = yeni ChildWithin ();
    }
  }
}

Gördüğünüz gibi, birisinin ebeveyn sınıfını miras almadan kullanmasına etkili bir şekilde izin verir.


7

Bu örnek iki dosya içerir: Assembly1.cs ve Assembly2.cs. İlk dosya dahili bir temel sınıf olan BaseClass'ı içerir. İkinci dosyada, BaseClass'ı örnekleme girişimi bir hata üretir.

// Assembly1.cs
// compile with: /target:library
internal class BaseClass 
{
   public static int intM = 0;
}

// Assembly1_a.cs
// compile with: /reference:Assembly1.dll
class TestAccess 
{
   static void Main()
   {  
      BaseClass myBase = new BaseClass();   // CS0122
   }
}

Bu örnekte, örnek 1'de kullandığınız dosyaları kullanın ve BaseClass'ın erişilebilirlik düzeyini genel olarak değiştirin . Ayrıca üye IntM'nin erişilebilirlik düzeyini dahili olarak değiştirin . Bu durumda, sınıfı başlatabilirsiniz, ancak dahili üyeye erişemezsiniz.

// Assembly2.cs
// compile with: /target:library
public class BaseClass 
{
   internal static int intM = 0;
}

// Assembly2_a.cs
// compile with: /reference:Assembly1.dll
public class TestAccess 
{
   static void Main() 
   {      
      BaseClass myBase = new BaseClass();   // Ok.
      BaseClass.intM = 444;    // CS0117
   }
}

kaynak : http://msdn.microsoft.com/tr-tr/library/7c5ka91b(VS.80).aspx


6

Mevcut derleme kapsamında erişilmesi gereken ve asla dışarıda olmayan yöntemlere, sınıflara vb. Sahip olduğunuzda.

Örneğin, bir DAL'in ORM'si olabilir, ancak nesneler iş katmanına maruz bırakılmamalıdır, tüm etkileşim statik yöntemlerle ve gerekli parametrelerden geçirilerek yapılmalıdır.


6

Çok ilginç bir iç kullanım - iç üyenin elbette sadece ilan edildiği meclis ile sınırlı olması - bir dereceye kadar "arkadaş" işlevselliği elde etmektir. Bir arkadaş üye, sadece bildirildiği meclis dışındaki diğer meclislerin görebileceği bir şeydir. C # arkadaş için yerleşik bir destek yok, ancak CLR var.

Bir arkadaş montajı bildirmek için InternalsVisibleToAttribute öğesini kullanabilirsiniz ve arkadaş montajından gelen tüm referanslar, arkadaş montajı kapsamında beyan meclisinizin dahili üyelerine herkese açık olarak davranacaktır. Bununla ilgili bir sorun, tüm dahili üyelerin görünür olmasıdır; seçip seçemezsiniz.

InternalsVisibleTo için iyi bir kullanım, çeşitli dahili üyeleri bir birim test düzeneğine maruz bırakmak ve böylece bu üyeleri test etmek için etraftaki karmaşık yansıma çalışmalarına olan ihtiyacı ortadan kaldırmaktır. Tüm iç üyelerin görünür olması pek bir sorun oluşturmaz, ancak bu yaklaşımı kullanmak sınıf arayüzlerinizi oldukça ağır bir şekilde emer ve ilan eden meclis içindeki kapsüllenmeyi mahvedebilir.


5

Genel kural olarak iki tür üye vardır:

  • genel yüzey : harici bir montajdan görülebilir (genel, korumalı ve dahili korumalı): arayan güvenilir değildir, bu nedenle parametre doğrulaması, yöntem dokümantasyonu vb. gereklidir.
  • özel yüzey : harici bir montajdan görünmez (özel ve dahili veya dahili sınıflar): arayan kişiye genellikle güvenilir, bu nedenle parametre doğrulaması, yöntem belgeleri vb. atlanabilir.

4

Gürültü azaltma, ne kadar az poz verirseniz kitaplığınız o kadar basit olur. Sabotaj prova / Güvenlik başka bir şeydir (Yansıma buna karşı kazansa da).


4

Dahili sınıflar, derlemenizin API'sını sınırlamanızı sağlar. Bunun, API'nızın anlaşılmasını kolaylaştırmak gibi avantajları vardır.

Ayrıca, montajınızda bir hata varsa, düzeltmenin kırılma değişikliği getirme şansı daha azdır. İç sınıflar olmadan, herhangi bir sınıfın kamu üyelerini değiştirmenin kırıcı bir değişiklik olacağını varsaymanız gerekir. İç sınıflarda, genel üyelerini değiştirmenin yalnızca derlemenin (ve InternalsVisibleTo özniteliğinde başvurulan tüm derlemeler) iç API'sini bozduğunu varsayabilirsiniz.

Sınıf düzeyinde ve montaj düzeyinde kapsülleme yapmayı seviyorum. Buna katılmayanlar var, ancak işlevselliğin mevcut olduğunu bilmek güzel.


2

Veri arka uç için LINQ-SQL kullanan bir proje var. İki ana ad alanım var: Biz ve Data. LINQ veri modeli Veri'de yaşar ve "dahili" olarak işaretlenir; Biz ad alanında LINQ veri sınıflarının etrafını saran ortak sınıflar bulunur.

Yani var Data.Clientve Biz.Client; ikincisi veri nesnesinin ilgili tüm özelliklerini ortaya koyar, örneğin:

private Data.Client _client;
public int Id { get { return _client.Id; } set { _client.Id = value; } }

Biz nesnelerinin özel bir kurucusu (fabrika yöntemlerinin kullanımını zorlamak için) ve şöyle görünen bir iç kurucusu vardır:

internal Client(Data.Client client) {
    this._client = client;
}

Bu, kütüphanedeki işletme sınıflarından herhangi biri tarafından kullanılabilir, ancak ön uç (UI) veri modeline doğrudan erişmenin bir yolu yoktur, bu da iş katmanının her zaman bir aracı görevi görmesini sağlar.

İlk defa internalçok fazla kullandım ve oldukça kullanışlı olduğunu kanıtladım.


2

Sınıfların üyelerini oluşturmanın mantıklı olduğu durumlar vardır internal. Bir örnek, sınıfların nasıl somutlaştırıldığını kontrol etmek istiyorsanız; diyelim ki sınıf örnekleri oluşturmak için bir çeşit fabrika sağlıyorsunuz. Oluşturucuyu yapabilirsiniz internal, böylece fabrika (aynı derlemede bulunan) sınıfın örneklerini oluşturabilir, ancak bu derlemenin dışındaki kodlar oluşturulamaz.

Ancak, ben sınıfları veya üye yapma herhangi bir noktayı göremiyorum internalbunları yapmak mantıklı olarak Sadece küçük gibi özel nedenlerle olmadan publicveya privatebelirli bir sebep olmaksızın.


1

dahili anahtar kelimeyi hiç kullandığım tek şey ürünümdeki lisans kontrol kodudur ;-)


1

Dahili anahtar kelimenin bir kullanımı, derlemenizin kullanıcısının somut uygulamalarına erişimi sınırlamaktır.

Nesneleri oluşturmak için bir fabrikanız veya başka bir merkezi konumunuz varsa, montajınızın kullanıcısı yalnızca genel arabirim veya soyut temel sınıfla ilgilenmelidir.

Ayrıca, iç yapıcılar, başka bir genel sınıfın nerede ve ne zaman başlatıldığını denetlemenize izin verir.


1

Buna ne dersiniz: Genellikle bir List nesnesini bir montajın harici kullanıcılarına göstermemeniz, bunun yerine bir IEnumerable'ı göstermeniz önerilir. Ancak dizi sözdizimini ve diğer tüm Liste yöntemlerini aldığınız için, derlemenin içinde bir List nesnesi kullanmak çok daha kolaydır. Yani, genellikle derleme içinde kullanılacak bir liste ortaya bir iç özellik var.

Bu yaklaşım hakkında yorumlarınızı bekliyoruz.


@Samik - lütfen üzerinde ayrıntılı bilgi verebilir misiniz, "genellikle bir List nesnesini bir montajın harici kullanıcılarına göstermemeniz, daha ziyade bir IEnumerable 'u göstermeniz önerilir." neden ienumerable tercih edilir?
Howiecamp

1
@Howiecamp: Çoğunlukla IEnumerable, listeye salt okunur bir erişim sağladığı için, listeyi doğrudan açığa çıkarmış gibi istemciler tarafından değiştirilebilen istemciler tarafından değiştirilebilir (örneğin, öğeleri silme vs.).
Samik R

Ayrıca, bir List veya IList'in teşhir edilmesi, verilere her zaman hızlı rastgele erişime izin vereceğiniz (örneğin) bir sözleşme oluşturur. Bir gün başka bir koleksiyon kullanmak için yönteminizi değiştirmeye karar verirseniz veya hiç koleksiyon (verim getirisi) vermezseniz, yalnızca bir değer döndürmek için bir Liste oluşturmanız veya yöntemin dönüş türünü değiştirerek sözleşmeyi kırmanız gerekir. Daha az spesifik bir dönüş türü kullanmak size esneklik sağlar.

1

publicBirisi proje ad alanınıza baktığında, olarak tanımlanan herhangi bir sınıfın otomatik olarak zekâda görüneceğini unutmayın. API perspektifinden bakıldığında, projenizin kullanıcılarına yalnızca kullanabilecekleri sınıfları göstermek önemlidir. internalGörmemesi gereken şeyleri gizlemek için anahtar kelimeyi kullanın .

Eğer senin Big_Important_ClassProje A projeniz dışında kullanılmak üzere tasarlanmıştır, o zaman işaretlemek olmamalıdır internal.

Ancak, birçok projede, genellikle sadece bir proje içinde kullanılmak üzere tasarlanmış sınıflara sahip olursunuz. Örneğin, parametreli bir iş parçacığı çağırma argümanlarını tutan bir sınıfınız olabilir. Bu durumlarda, internalkendinizi yolda istenmeyen bir API değişikliğinden korumaktan başka bir sebep yokmuş gibi işaretlemelisiniz .


Öyleyse neden özel kullanmıyorsunuz?
Mahkum SIFIR

1
privateAnahtar kelime yalnızca başka sınıf veya yapı içinde tanımlanan sınıfları veya yapılar üzerinde de kullanılabilir. Bir ad alanı içinde tanımlanan bir sınıf yalnızca publicveya olarak bildirilebilir internal.
Matt Davis

1

Buradaki fikir, bir kütüphane tasarlarken yalnızca dışarıdan (kütüphanenizin müşterileri tarafından) kullanılması amaçlanan sınıfların herkese açık olması gerektiğidir. Bu şekilde

  1. Gelecekteki sürümlerde değişme olasılığı vardır (herkese açık olsalardı müşteri kodunu kırarsınız)
  2. Müşteri için işe yaramaz ve karışıklığa neden olabilir
  3. Güvenli değil (yanlış kullanım kitaplığınızı oldukça bozabilir)

vb.

İç unsurları kullanmaktan ziyade şirket içi çözümler geliştiriyorsanız, o kadar önemli değil sanırım, çünkü genellikle müşteriler sizinle sürekli iletişim kuracak ve / veya koda erişebilecektir. Yine de kütüphane geliştiricileri için oldukça kritiktirler.


-7

Nesneye Dayalı Paradigma tam olarak uymayan, tehlikeli şeyler yapan, kontrolünüz altındaki diğer sınıflardan ve yöntemlerden çağrılması gereken ve başkalarının kullanmasına izin vermek istemediğiniz sınıflarınız veya yöntemleriniz olduğunda .

public class DangerousClass {
    public void SafeMethod() { }
    internal void UpdateGlobalStateInSomeBizarreWay() { }
}

Ne demeye çalışıyorsun? Çok net değil. En azından bana değil.
pqsk

@Pqsk ile aynı fikirde
Anynomous Khan
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.