Neden IList veya List kullanmalısınız?


82

Bu konuda çok fazla gönderi olduğunu biliyorum ama yine de kafamı karıştırıyor, neden IList gibi bir arayüze geçip somut liste yerine IList gibi bir arayüz geri döndürmelisiniz.

Bunun daha sonra uygulamayı değiştirmeyi nasıl kolaylaştırdığını söyleyen birçok yazı okudum, ancak bunun nasıl çalıştığını tam olarak anlamıyorum.

Bu yönteme sahip olduğumu söyle

  public class SomeClass
    {
        public bool IsChecked { get; set; }
    }

 public void LogAllChecked(IList<SomeClass> someClasses)
    {
        foreach (var s in someClasses)
        {
            if (s.IsChecked)
            {
                // log 
            }
        }
    }

IList'i kullanmanın gelecekte bana nasıl yardımcı olacağından emin değilim.

Zaten yöntemde olsam nasıl olur? Yine de IList kullanmalı mıyım?

public void LogAllChecked(IList<SomeClass> someClasses)
    {
        //why not List<string> myStrings = new List<string>()
        IList<string> myStrings = new List<string>();

        foreach (var s in someClasses)
        {
            if (s.IsChecked)
            {
                myStrings.Add(s.IsChecked.ToString());
            }
        }
    }

IList'i şimdi kullandığım için ne kazanacağım?

public IList<int> onlySomeInts(IList<int> myInts)
    {
        IList<int> store = new List<int>();
        foreach (var i in myInts)
        {
            if (i % 2 == 0)
            {
                store.Add(i);
            }
        }

        return store;
    }

Şimdi nasıl? Değiştirmem gereken int'lerin bir listesinin yeni bir uygulaması var mı?

Temel olarak, IList kullanmanın List'i her şeyin içine almak yerine bazı sorunları nasıl çözeceğine dair bazı gerçek kod örneklerini görmem gerekiyor.

Okumalarıma göre IList yerine IEnumberable kullanabileceğimi düşünüyorum çünkü sadece bir şeyler arasında dolaşıyorum.

Düzenle Bu yüzden, bunu nasıl yapacağımla ilgili bazı yöntemlerimle uğraşıyordum. Dönüş türünden hala emin değilim (daha somut veya bir arayüz yapmam gerekirse).

 public class CardFrmVm
    {
        public IList<TravelFeaturesVm> TravelFeaturesVm { get; set; }
        public IList<WarrantyFeaturesVm> WarrantyFeaturesVm { get; set; }

        public CardFrmVm()
        {
            WarrantyFeaturesVm = new List<WarrantyFeaturesVm>();
            TravelFeaturesVm = new List<TravelFeaturesVm>();
        }
}

 public class WarrantyFeaturesVm : AvailableFeatureVm
    {
    }

 public class TravelFeaturesVm : AvailableFeatureVm
    {
    }

 public class AvailableFeatureVm
    {
        public Guid FeatureId { get; set; }
        public bool HasFeature { get; set; }
        public string Name { get; set; }
    }


        private IList<AvailableFeature> FillAvailableFeatures(IEnumerable<AvailableFeatureVm> avaliableFeaturesVm)
        {
            List<AvailableFeature> availableFeatures = new List<AvailableFeature>();
            foreach (var f in avaliableFeaturesVm)
            {
                if (f.HasFeature)
                {
                                                    // nhibernate call to Load<>()
                    AvailableFeature availableFeature = featureService.LoadAvaliableFeatureById(f.FeatureId);
                    availableFeatures.Add(availableFeature);
                }
            }

            return availableFeatures;
        }

Şimdi basit gerçeği için IList'i geri veriyorum, daha sonra bunu etki alanı modelime böyle bir özelliğe sahip olanı ekleyeceğim:

public virtual IList<AvailableFeature> AvailableFeatures { get; set; }

Yukarıdakiler bir IList'in kendisidir, çünkü bu, nhibernate ile kullanılacak standart gibi görünmektedir. Aksi takdirde IEnumberable'ı geri döndürebilirim ama emin değilim. Yine de, kullanıcının% 100 neye ihtiyaç duyacağını bulamıyorum (bu, bir betonu döndürmenin avantajlı olduğu yer).

Düzenle 2

Yöntemimde referans olarak geçmek istersem ne olur diye de düşünüyordum.

private void FillAvailableFeatures(IEnumerable<AvailableFeatureVm> avaliableFeaturesVm, IList<AvailableFeature> toFill)
            {

                foreach (var f in avaliableFeaturesVm)
                {
                    if (f.HasFeature)
                    {
                                                        // nhibernate call to Load<>()
                        AvailableFeature availableFeature = featureService.LoadAvaliableFeatureById(f.FeatureId);
                        toFill.Add(availableFeature);
                    }
                }
            }

bununla sorun yaşar mıyım? Ne zamandan beri bir diziyi (sabit boyutu olan) geçemezler? Somut bir Liste için belki daha iyi olur mu?

Yanıtlar:


156

Burada üç soru var: Biçimsel bir parametre için ne tür kullanmalıyım? Yerel bir değişken için ne kullanmalıyım? ve bir dönüş türü için ne kullanmalıyım?

Biçimsel parametreler:

Buradaki ilke, ihtiyacınız olandan fazlasını istememektir . IEnumerable<T>"Bu dizinin öğelerini başından sonuna kadar almam gerekiyor" iletisini iletir. IList<T>"Bu dizinin öğelerini rasgele sırada almam ve ayarlamam gerekiyor" iletisini iletir. List<T>"Bu dizinin öğelerini rasgele sırada almam ve ayarlamam gerekiyor ve yalnızca listeleri kabul ediyorum; dizileri kabul etmiyorum."

İhtiyaç duyduğunuzdan fazlasını isteyerek, (1) arayanın gereksiz taleplerinizi karşılamak için gereksiz iş yapmasını sağlarsınız ve (2) okuyucuya yanlışları iletirsiniz. Yalnızca ne kullanacağınızı sorun. Bu şekilde, arayanın bir sıralaması varsa, talebinizi karşılamak için ToList'i aramasına gerek yoktur.

Yerel değişkenler:

Ne istersen onu kullan. Bu senin metodun. Yöntemin dahili uygulama ayrıntılarını görebilen tek kişi sizsiniz.

Dönüş türü:

Öncekiyle aynı prensip tersine çevrildi. Arayan kişinin ihtiyaç duyduğu minimum miktarı sunun. Arayan kişi yalnızca diziyi numaralandırma becerisine ihtiyaç duyuyorsa, onlara yalnızca bir IEnumerable<T>.


9
Arayanın neye ihtiyacı olduğunu nasıl anlarsınız? Örneğin, dönüş türlerimden birini bir IList <> olarak değiştiriyordum, o zaman muhtemelen onları numaralandıracağım, yine de bir IEnumberable döndürelim. Sonra kendi görüşüme (mvc) baktım ve bir for döngüsü kullanmam gerektiğinden sayma yöntemine gerçekten ihtiyacım olduğunu öğrendim. Bu yüzden kendi uygulamamda gerçekte neye ihtiyacım olduğunu tahmin ettim, başka birinin neye ihtiyaç duyacağını veya neye ihtiyaç duymayacağını nasıl tahmin edersiniz?
chobo2

@ chobo2: özel örnekte, LINQ CountO (1) çalışır eğer senin IEnumerablebir olduğunu ICollection. Tabii ki, sadece bir foreachdöngü de kullanabilirsiniz .
Brian

6
@ chobo2: Arayanın hangi yöntemlere ihtiyaç duyacağını nasıl tahmin edersiniz ? İlk önce çözülmesi gereken sorun bu gibi görünüyor. Muhtemelen bir şekilde onları arayacak olan insanlar için hangi yöntemleri yazacağınızı bilmenin bir yolunu buluyorsunuz. Bu insanlara geri dönme yöntemlerinin ne olmasını istediklerini sorun. Sorunuz temelde "hangi yazılımı yazacağımı nasıl bilebilirim?" Müşterinizin çözmesi gereken sorunları öğrenerek ve sorunlarını çözen kod yazarak bilirsiniz.
Eric Lippert

1
@Eric - yalnızca bir öğe eklemeniz gereken bir ICollection örneğini içerecek şekilde biçimsel parametrelere ilişkin yanıtınızı güncellemelisiniz. Ayrıca dönüş türü açıklamanız, 'arayanın gerçekleştirmesine izin verdiğinizin yalnızca minimumunu sunun' satırları boyunca bir şey söylemelidir. Salt okunur bir liste ise, yalnızca IEnumerable, vb. Döndürür.
Charles Lambert

4
@kvb: Neredeyse. İlk senaryonuzu düşünün. Yaparak algoritmik bir iyileştirme yapabilirsiniz items as IList<T>ve boş olmazsa, iyileştirilmiş kodunuzu kullanın. Bu şekilde, müşteriye teslim ettiklerinde esneklik sağlarken, yapabiliyorsanız avantaj elde edersiniz.
Eric Lippert

33

Şimdiye kadar gördüğüm en pratik neden, Jeffrey Richter tarafından CLR'de C # aracılığıyla verildi.

Kalıp, argümanlarınız için mümkün olan en temel sınıfı veya arayüzü almak ve dönüş türleriniz için mümkün olan en spesifik sınıfı veya arayüzü döndürmektir. Bu, arayanlarınıza türleri yöntemlerinize geçirme konusunda en fazla esnekliği ve dönüş değerlerini dönüştürme / yeniden kullanma fırsatı verir.

Örneğin, aşağıdaki yöntem

public void PrintTypes(IEnumerable items) 
{ 
    foreach(var item in items) 
        Console.WriteLine(item.GetType().FullName); 
}

yöntemin numaralandırılabilir herhangi bir türde geçiş olarak adlandırılmasına izin verir . Daha spesifik olsaydın

public void PrintTypes(List items)

o zaman, diyelim ki, bir diziniz varsa ve tür adlarını konsola yazdırmak istiyorsanız, önce yeni bir Liste oluşturmanız ve onu türlerinizle doldurmanız gerekir. Ve genel bir uygulama kullandıysanız, yalnızca belirli türdeki nesnelerle herhangi bir nesne için çalışan bir yöntemi kullanabilirsiniz .

Dönüş türleri hakkında konuşurken, ne kadar spesifik olursanız, arayanlar o kadar esnek olabilir.

public List<string> GetNames()

isimleri yinelemek için bu dönüş türünü kullanabilirsiniz

foreach(var name in GetNames())

veya doğrudan koleksiyona indeksleyebilirsiniz

Console.WriteLine(GetNames()[0])

Oysa, daha az spesifik bir türü geri alıyorsanız

public IEnumerable GetNames()

ilk değeri elde etmek için dönüş türüne masaj yapmanız gerekir

Console.WriteLine(GetNames().OfType<string>().First());

10
Bu tavsiyenin, Eric Lippert'in iade türleri tavsiyesinde verdiği yanıtla çeliştiğini unutmayın. Jeffrey Richter'in yaklaşımı, yöntemin tüketicilerine iade edilen nesneyi istedikleri gibi kullanma konusunda en fazla esnekliği sağlarken, Eric'in yöntemi, yöntemin genel yüzeyini değiştirmeden uygulamayı değiştirme konusunda en fazla esnekliği sağlar. Jeffrey'nin dahili kod konusundaki tavsiyelerine uyma eğilimindeyim, ancak bir halk kütüphanesi için Eric'inkini takip etmeye muhtemelen daha meyilli olurdum.
phoog

3
@phoog: Eric'in nereden geldiğini düşünürsek, değişiklikleri bozma konusunda daha temkinli davranması şaşırtıcı olmaz. Ama kesinlikle geçerli bir nokta.

Çok sert. Görünüşe göre her iki taraf için de insanlar var (en az geri dönün veya dönmeyin). Gereksiz dönüştürmeyi durdurmaya yardımcı olan parametreler için bunu neden yaptığınızı daha fazla anlıyorum. Dönüş türü ile ne yapacağımdan hala emin değilim.
chobo2

@ chobo2: Gerçekten, genelden daha kesin bir şey belirtirseniz, gelecekte dönüş türünü değiştirmeniz gerekip gerekmediğini düşünmeniz yeterlidir. Yönteminizi düşünebilirseniz, muhtemelen iade toplama türünü değiştirmeyeceğinizi belirleyin, o zaman daha kesin bir tür döndürmek muhtemelen güvenlidir. Emin değilseniz ya da ileride değiştirirseniz başkalarının kodunu kıracağınızdan korkuyorsanız, o zaman daha genel gidin.

1
+1 Trivia: Bu yanıt, Postel'in Yasasının CLR'ye özgü güzel bir örneğidir . :)
Dan J

13

IEnumerable<T>bir koleksiyon boyunca yineleme yapmanızı sağlar. ICollection<T>bunun üzerine inşa edilir ve ayrıca öğe ekleme ve çıkarma işlemlerine de izin verir. IList<T>ayrıca belirli bir dizinde bunlara erişmeye ve değiştirmeye izin verir. Tüketicinizin birlikte çalışmasını beklediğinizi açığa çıkararak, uygulamanızı değiştirmekte özgürsünüz. List<T>bu arayüzlerin üçünü de uygular.

Mülkünüzü bir List<T>veya hatta bir IList<T>zaman olarak ifşa ederseniz , tüketicinizin sahip olmasını istediğiniz tek şey, koleksiyon boyunca yineleme yeteneğidir. Daha sonra listeyi değiştirebilecekleri gerçeğine güvenebilirler. Eğer bir gerçek veri deposunu dönüştürmeye karar Sonra sonra List<T>bir karşı Dictionary<T,U>özellik için gerçek değer olarak sözlük anahtarları ve açığa (Ben tam olarak bundan önce yapmak zorunda). O zaman, değişikliklerinin sınıfınıza yansıtılmasını bekleyen tüketiciler artık bu yeteneğe sahip olmayacak. Bu büyük bir problem! Bir List<T>olarak ortaya koyarsanız, IEnumerable<T>koleksiyonunuzun harici olarak değiştirilmeyeceğini rahatlıkla tahmin edebilirsiniz. Bu, List<T>yukarıdaki arayüzlerden herhangi biri olarak ifşa etmenin güçlerinden biridir .

Bu düzeydeki soyutlama, yöntem parametrelerine ait olduğunda diğer yöne gider. Listenizi kabul eden bir yönteme aktardığınızda, listenizin değiştirilmeyeceğinden IEnumerable<T>emin olabilirsiniz. Yöntemi uygulayan kişi olduğunuzda ve kabul ettiğinizi söylediğinizde IEnumerable<T>yapmanız gereken tek şey bu listeyi yinelemek. O zaman yöntemi çağıran kişi, onu numaralandırılabilir herhangi bir veri türü ile çağırmakta özgürdür. Bu, kodunuzun beklenmedik ancak tamamen geçerli şekillerde kullanılmasına izin verir.

Bundan, yöntem uygulamanızın yerel değişkenlerini dilediğiniz gibi temsil edebileceği anlaşılır. Uygulama ayrıntıları ifşa edilmemiştir. Kodunuzu çağıran insanları etkilemeden kodunuzu daha iyi bir şeye değiştirme özgürlüğü.

Geleceği tahmin edemezsiniz. Bir mülkün türünün her zaman yararlı olacağını List<T>varsaymak, kodunuzla ilgili öngörülemeyen beklentilere uyum sağlama yeteneğinizi derhal sınırlandırır. Evet, bu veri türünü asla a'dan değiştiremezsiniz, List<T>ancak mecbur kalırsanız emin olabilirsiniz. Kodunuz bunun için hazır.


9

Kısa cevap:

Arayüzü, o arayüzün hangi somut uygulamasını kullanırsanız kullanın, kodunuz onu destekleyecek şekilde geçirirsiniz.

Somut bir liste uygulaması kullanırsanız, aynı listenin başka bir uygulaması kodunuz tarafından desteklenmeyecektir.

Kalıtım ve çok biçimlilik hakkında biraz okuyun .


8

İşte bir örnek: Bir zamanlar listelerimizin çok büyüdüğü bir projem vardı ve bu büyük nesne yığınının parçalanmasıyla sonuçlanan performansa zarar veriyordu. List'i LinkedList ile değiştirdik. LinkedList bir dizi içermez, dolayısıyla birdenbire, büyük nesne yığınını neredeyse hiç kullanmadık.

Çoğunlukla listeleri IEnumerable<T>zaten olduğu gibi kullandık , dolayısıyla başka bir değişikliğe gerek yoktu. (Ve evet, eğer yaptığınız tek şey onları numaralandırmaksa referansları IEnumerable olarak ilan etmenizi tavsiye ederim.) Birkaç yerde liste indeksleyicisine ihtiyacımız vardı, bu yüzden IList<T>bağlantılı listelerin etrafına verimsiz bir paket yazdık . Liste indeksleyiciye nadiren ihtiyaç duyduk, bu nedenle verimsizlik bir sorun değildi. Öyle olsaydı, IList'in başka bir uygulamasını sağlayabilirdik, belki de yeterince küçük dizilerin bir koleksiyonu olarak, büyük nesnelerden kaçınarak daha verimli bir şekilde indekslenebilir olurdu.

Sonunda, herhangi bir nedenle bir uygulamayı değiştirmeniz gerekebilir; performans sadece bir olasılıktır. Nedeni ne olursa olsun, mümkün olan en az türetilmiş türü kullanmak, nesnelerinizin belirli çalışma zamanı türünü değiştirdiğinizde kodunuzda değişiklik yapma ihtiyacını azaltacaktır.


3

Yöntemin içinde, veya varyerine kullanmalısınız . Veri kaynağınız bunun yerine bir yöntemden gelmek üzere değiştiğinde, yönteminiz hayatta kalacaktır.IListListonlySomeInts

Parametre olarak kullanmak IListyerine kullanılmasının nedeni List, birçok şeyin uygulanması IList(iki örnek olarak Liste ve []), ancak yalnızca bir şeyin uygulanmasıdır List. Arayüze kod yazmak daha esnektir.

Sadece değerler üzerinden numaralandırıyorsanız, kullanmalısınız IEnumerable. Birden fazla değeri tutabilen her veri türü, uygular IEnumerable(veya gerekir) ve yönteminizi son derece esnek hale getirir.


13
"Var" ı kullanması gerektiğini söylemek tamamen yanlış. Orada ne kullandığı önemli değil - bu bir stil meselesi. Yöntemin imzasını etkilemez ve derleme zamanında sabittir. Bunun yerine, yerelini IList foo = new List gibi ilan etme konusundaki kafa karışıklığının üstesinden gelmesine yardım etmelisiniz - bu onun kafa karışıklığının açıkça yattığı yerdir.
x0n

Yani temelde IList'i, sırf bir dizi göndermek istiyorlarsa, yapmak zorunda olmadıkları bir diziyi almak için mi söylüyorsunuz? Geri vermeye ne dersin? Vars kullanırken "yöntem hayatta kalacak" derken neyi kastettiğinizi anlamıyorum? Biraz daha fazla iş olacağını biliyorum ama bunu da yeni tiple değiştirmen gerekmeyecek mi? Yani IList <int> için IList <String> olabilir mi?
chobo2

Doğru, "var kullanımı" nın amacı, yöntemin kendi içinde onun için endişelenmemek ve tüketicilere nasıl göründüğüne daha çok odaklanmak için bir öneriydi.
Bryan Boettcher

@ X0n: var'ın aşırı kullanıldığına katılıyorum ve herhangi bir şeyi daha net hale getirmeye yardımcı olmayacak.
Şu Chuck Guy

1

Liste yerine IList kullanmak, birim testleri yazmayı önemli ölçüde kolaylaştırır. Verileri iletmek ve iade etmek için bir 'Alaycı' kitaplığı kullanmanıza olanak tanır.

Arayüzleri kullanmanın diğer genel nedeni, bir nesnenin kullanıcısına gerekli olan minimum bilgi miktarını ifşa etmektir.

IList'i uygulayan bir veri nesnesine sahip olduğum (yapmacık) durumu düşünün.

public class MyDataObject : IList<int>
{
    public void Method1()
    {
       ...
    }
    // etc
}

Yukarıdaki işlevleriniz yalnızca bir liste üzerinde yineleme yapabilmeyi önemsiyor. İdeal olarak, bu listeyi kimin uyguladığını veya nasıl uyguladıklarını bilmeleri gerekmemelidir.

Örneğinizde, düşündüğünüz gibi IEnumerable daha iyi bir seçimdir.


1

Kodunuz arasındaki bağımlılıkları olabildiğince azaltmak her zaman iyi bir fikirdir.

Bunu akılda tutarak, mümkün olan en az sayıda dış bağımlılığa sahip türleri geçmek ve aynı şeyi döndürmek en mantıklıdır. Ancak bu, yöntemlerinizin görünürlüğüne ve imzalarına bağlı olarak farklı olabilir.

Yöntemleriniz bir arabirimin bir parçasını oluşturuyorsa, yöntemlerin o arabirimde bulunan türler kullanılarak tanımlanması gerekecektir. Beton türleri muhtemelen arabirimler için mevcut olmayacak, bu nedenle somut olmayan türleri döndürmeleri gerekecektir. Örneğin bir çerçeve oluşturuyor olsaydınız bunu yapmak istersiniz.

Bununla birlikte, bir çerçeve yazmıyorsanız, parametreyi olası en zayıf türlerle (yani temel sınıflar, arabirimler ve hatta temsilciler) geçirmek ve somut türleri döndürmek avantajlı olabilir. Bu, arayan kişiye, bir arabirim olarak kullanılsa bile, döndürülen nesne ile mümkün olduğunca çok şey yapma yeteneği verir. Ancak, döndürülen nesne türündeki herhangi bir değişiklik çağıran kodu bozabileceğinden, bu yöntem daha kırılgan hale getirir. Pratikte olsa da, bu genellikle büyük bir sorun değildir.


1

Arayanı bağımsız değişken olarak farklı somut türleri göndermesine izin verdiği için bir Arayüzü bir yöntem için parametre olarak kabul edersiniz. Örnek yönteminiz LogAllChecked verildiğinde, someClasses parametresi çeşitli türlerde olabilir ve yöntemi yazan kişi için tümü eşdeğer olabilir (yani, parametrenin türünden bağımsız olarak tam olarak aynı kodu yazarsınız). Ancak yöntemi çağıran kişi için çok büyük bir fark yaratabilir - eğer bir dizileri varsa ve bir liste istiyorsanız, yöntemi her çağırdığında diziyi bir liste veya vv olarak değiştirmeleri gerekir, hem programcıdan hem de performans bakış açısıyla gelen süre.

Bir Arayüz mü yoksa somut bir tür mi iade edeceğiniz, arayanların oluşturduğunuz nesneyle ne yapmasına izin vermek istediğinize bağlıdır - bu bir API tasarım kararıdır ve zor ve hızlı bir kural yoktur. Nesneyi tam olarak kullanma yeteneklerini, nesnelerin işlevselliğinin bir bölümünü kolayca kullanma yetenekleriyle (ve tabii ki, nesneyi tam olarak kullanmalarını İSTEMEZ) karşılaştırmanız gerekir. Örneğin, bir IEnumerable döndürürseniz, onları yineleme ile sınırlandırmış olursunuz - nesnenizden öğe ekleyemez veya kaldıramazlar, yalnızca nesnelere karşı hareket edebilirler. Bir sınıfın dışında bir koleksiyonu ifşa etmeniz gerekiyorsa, ancak arayanın koleksiyonu değiştirmesine izin vermek istemiyorsanız, bu, bunu yapmanın bir yoludur. Öte yandan, doldurmasını beklediğiniz / istediğiniz boş bir koleksiyon iade ediyorsanız,


0

İşte bu .NET 4.5+ dünyasındaki cevabım .

IList <T> kullanın ve IReadonlyList <T> ,
              yerine List <T> , çünkü ReadonlyList <T> yok.

IList <T> IReadonlyList <T> ile çok tutarlı görünüyor

  • Kullanım IEnumerable <T> asgari karşılaşması (mülkiyet) ya da gereksinimi (parametre) için ise foreach kullanmak tek yoldur.
  • Kullanım IReadonlyList <T> da / kullanımı ortaya çıkarmak için gerekirse Kont ve [] dizinleyiciyi.
  • Arayanların öğe eklemesine / güncellemesine / silmesine de izin veriyorsanız IList <T> kullanın

Çünkü listesi <T> uygular IReadonlyList <T> , herhangi bir açık döküm ihtiyacı yoktur.

Örnek bir sınıf:

// manipulate the list within the class
private List<int> _numbers;

// callers can add/update/remove elements, but cannot reassign a new list to this property
public IList<int> Numbers { get { return _numbers; } }

// callers can use: .Count and .ReadonlyNumbers[idx], but cannot add/update/remove elements
public IReadOnlyList<int> ReadonlyNumbers { get { return _numbers; } }
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.