Tại sao tôi không thể xác định hàm tạo mặc định cho cấu trúc trong .NET?


261

Trong .NET, một loại giá trị (C # struct ) không thể có hàm tạo không có tham số. Theo bài đăng này, điều này được ủy quyền bởi đặc tả CLI. Điều gì xảy ra là với mỗi loại giá trị, một hàm tạo mặc định được tạo (bởi trình biên dịch?) Đã khởi tạo tất cả các thành viên về 0 (hoặc null).

Tại sao nó không được phép định nghĩa một hàm tạo mặc định như vậy?

Một cách sử dụng tầm thường là cho các số hữu tỷ:

public struct Rational {
    private long numerator;
    private long denominator;

    public Rational(long num, long denom)
    { /* Todo: Find GCD etc. */ }

    public Rational(long num)
    {
        numerator = num;
        denominator = 1;
    }

    public Rational() // This is not allowed
    {
        numerator = 0;
        denominator = 1;
    }
}

Sử dụng phiên bản hiện tại của C #, một Rational mặc định là 0/0 không thú vị lắm.

PS : Các tham số mặc định sẽ giúp giải quyết điều này cho C # 4.0 hay hàm tạo mặc định do CLR xác định sẽ được gọi?


Jon Skeet trả lời:

Để sử dụng ví dụ của bạn, điều bạn muốn xảy ra khi ai đó đã làm:

 Rational[] fractions = new Rational[1000];

Nó có nên chạy qua constructor của bạn 1000 lần không?

Chắc chắn nó nên, đó là lý do tại sao tôi đã viết hàm tạo mặc định ở vị trí đầu tiên. CLR nên sử dụng hàm tạo zeroing mặc định khi không có hàm tạo mặc định rõ ràng nào được xác định; theo cách đó bạn chỉ trả tiền cho những gì bạn sử dụng. Sau đó, nếu tôi muốn một thùng chứa 1000 Rationals không mặc định (và muốn tối ưu hóa 1000 công trình), tôi sẽ sử dụng một List<Rational>thay vì một mảng.

Lý do này, trong tâm trí của tôi, không đủ mạnh để ngăn chặn định nghĩa của một nhà xây dựng mặc định.


3
+1 đã có một vấn đề tương tự một lần, cuối cùng đã chuyển đổi cấu trúc thành một lớp.
Dirk Vollmar

4
Các tham số mặc định trong C # 4 không thể trợ giúp vì Rational()gọi ctor không tham số thay vì Rational(long num=0, long denom=1).
LaTeX

6
Lưu ý rằng trong C # 6.0 đi kèm với Visual Studio 2015, nó sẽ được phép viết các hàm tạo đối tượng tham số zero cho các cấu trúc. Vì vậy, new Rational()sẽ gọi hàm tạo nếu nó tồn tại, tuy nhiên nếu nó không tồn tại, new Rational()sẽ tương đương với default(Rational). Trong mọi trường hợp, bạn được khuyến khích sử dụng cú pháp default(Rational)khi bạn muốn "giá trị 0" của cấu trúc của bạn (đó là một số "xấu" với thiết kế được đề xuất của bạn Rational). Giá trị mặc định cho một loại giá trị Tluôn luôn default(T). Vì vậy, new Rational[1000]sẽ không bao giờ gọi các nhà xây dựng cấu trúc.
Jeppe Stig Nielsen

6
Để giải quyết vấn đề cụ thể này, bạn có thể lưu trữ denominator - 1bên trong cấu trúc, để giá trị mặc định trở thành 0/1
miniBill

3
Then if I want a container of 1000 non-default Rationals (and want to optimize away the 1000 constructions) I will use a List<Rational> rather than an array.Tại sao bạn lại mong đợi một mảng sẽ gọi một hàm tạo khác đến một Danh sách cho một cấu trúc?
mjwills

Câu trả lời:


197

Lưu ý: các câu trả lời dưới đây được viết một thời gian dài trước khi C # 6, mà đang có kế hoạch để giới thiệu khả năng nhà xây dựng parameterless khai báo trong cấu trúc - nhưng họ vẫn sẽ không được gọi trong mọi tình huống (ví dụ như để tạo mảng) (cuối cùng tính năng này không được thêm vào C # 6 ).


EDIT: Tôi đã chỉnh sửa câu trả lời dưới đây do cái nhìn sâu sắc của Grauenwolf về CLR.

CLR cho phép các loại giá trị có các hàm tạo không tham số, nhưng C # thì không. Tôi tin rằng điều này là bởi vì nó sẽ đưa ra một kỳ vọng rằng các nhà xây dựng sẽ được gọi khi nó sẽ không. Ví dụ, hãy xem xét điều này:

MyStruct[] foo = new MyStruct[1000];

CLR có thể thực hiện việc này rất hiệu quả chỉ bằng cách phân bổ bộ nhớ phù hợp và loại bỏ tất cả. Nếu nó phải chạy hàm tạo MySturation 1000 lần, điều đó sẽ kém hiệu quả hơn rất nhiều. (Trên thực tế, nó không - nếu bạn làm có một constructor parameterless, nó không được chạy khi bạn tạo một mảng, hoặc khi bạn có một biến Ví dụ uninitialized.)

Quy tắc cơ bản trong C # là "giá trị mặc định cho mọi loại không thể dựa vào bất kỳ khởi tạo nào". Bây giờ họ có thể đã cho phép các hàm tạo không tham số được xác định, nhưng sau đó không yêu cầu hàm tạo đó được thực thi trong mọi trường hợp - nhưng điều đó sẽ dẫn đến nhiều nhầm lẫn hơn. (Hoặc ít nhất, vì vậy tôi tin rằng cuộc tranh luận diễn ra.)

EDIT: Để sử dụng ví dụ của bạn, điều gì bạn muốn xảy ra khi ai đó đã làm:

Rational[] fractions = new Rational[1000];

Nó có nên chạy qua constructor của bạn 1000 lần không?

  • Nếu không, chúng tôi kết thúc với 1000 lý do không hợp lệ
  • Nếu vậy, chúng ta có khả năng lãng phí một khối lượng công việc nếu chúng ta sắp điền vào mảng với các giá trị thực.

EDIT: (Trả lời thêm một chút câu hỏi) Hàm tạo không tham số không được tạo bởi trình biên dịch. Các loại giá trị không cần phải có các nhà xây dựng theo như CLR có liên quan - mặc dù hóa ra nó có thể nếu bạn viết nó trong IL. Khi bạn viết " new Guid()" trong C # phát ra IL khác với những gì bạn nhận được nếu bạn gọi một hàm tạo bình thường. Xem câu hỏi SO này để biết thêm một chút về khía cạnh đó.

Tôi nghi ngờ rằng không có bất kỳ loại giá trị nào trong khung với các hàm tạo không tham số. Không nghi ngờ gì nữa, NDepend có thể cho tôi biết nếu tôi hỏi nó đủ độc đáo ... Thực tế là C # cấm nó là một gợi ý đủ lớn để tôi nghĩ rằng đó có lẽ là một ý tưởng tồi.


9
Giải thích ngắn hơn: Trong C ++, struct và class chỉ là hai mặt của cùng một đồng tiền. Sự khác biệt thực sự duy nhất là một cái được công khai theo mặc định và cái khác là riêng tư. Trong .Net, có một sự khác biệt lớn hơn nhiều giữa một cấu trúc và một lớp và điều quan trọng là phải hiểu nó.
Joel Coehoorn

39
@Joel: Điều đó không thực sự giải thích hạn chế cụ thể này, phải không?
Jon Skeet

6
CLR không cho phép các loại giá trị có các hàm tạo không tham số. Và vâng, nó sẽ chạy nó cho từng phần tử trong một mảng. C # nghĩ rằng đây là một ý tưởng tồi và không cho phép nó, nhưng bạn có thể viết một ngôn ngữ .NET.
Jonathan Allen

2
Xin lỗi, tôi hơi bối rối với những điều sau đây. Không Rational[] fractions = new Rational[1000];còn lãng phí một tải công việc nếu Rationallà một lớp học thay vì một struct? Nếu vậy, tại sao các lớp có một ctor mặc định?
hôn vào nách của tôi

5
@ FifaEarthCup2014: Bạn sẽ phải cụ thể hơn về ý của bạn bằng cách "lãng phí một khối lượng công việc". Nhưng dù bằng cách nào, nó sẽ không gọi hàm tạo 1000 lần. Nếu Rationallà một lớp, bạn sẽ kết thúc với một mảng 1000 tài liệu tham khảo null.
Jon Skeet

48

Một cấu trúc là một loại giá trị và một loại giá trị phải có một giá trị mặc định ngay khi nó được khai báo.

MyClass m;
MyStruct m2;

Nếu bạn khai báo hai trường như trên mà không khởi tạo một trong hai, thì phá vỡ trình gỡ lỗi, msẽ là null nhưng m2sẽ không. Vì điều này, một hàm tạo không tham số sẽ không có ý nghĩa, trên thực tế, tất cả các hàm tạo trên một cấu trúc đều gán các giá trị, chính nó đã tồn tại chỉ bằng cách khai báo nó. Thật vậy, m2 có thể được sử dụng khá vui vẻ trong ví dụ trên và có các phương thức được gọi, nếu có, và các trường và thuộc tính của nó bị thao túng!


3
Không chắc chắn tại sao ai đó bỏ phiếu cho bạn xuống. Bạn dường như là câu trả lời đúng nhất ở đây.
pipTheGeek

12
Hành vi trong C ++ là nếu một kiểu có hàm tạo mặc định thì nó được sử dụng khi một đối tượng như vậy được tạo mà không có hàm tạo rõ ràng. Điều này có thể đã được sử dụng trong C # để khởi tạo m2 với hàm tạo mặc định, đó là lý do tại sao câu trả lời này không hữu ích.
Motti

3
onester: nếu bạn không muốn các cấu trúc gọi hàm tạo của riêng chúng khi được khai báo, thì đừng định nghĩa hàm tạo mặc định như vậy! :) đó là câu nói của Motti
Stefan Monov

8
@Tarik. Tôi không đồng ý. Ngược lại, một hàm tạo không tham số sẽ có ý nghĩa đầy đủ: nếu tôi muốn tạo một cấu trúc "Ma trận" luôn có một ma trận danh tính làm giá trị mặc định, làm thế nào bạn có thể làm điều đó bằng cách khác?
Elo

1
Tôi không chắc chắn tôi hoàn toàn đồng ý với "Thật vậy, m2 có thể được sử dụng khá vui vẻ .." . Nó có thể đúng trong một C # trước nhưng đó là lỗi trình biên dịch khi khai báo một cấu trúc, không phải newnó, sau đó thử sử dụng các thành viên của nó
Caius Jard

18

Mặc dù CLR cho phép nó, C # không cho phép các cấu trúc có một hàm tạo không tham số mặc định. Lý do là, đối với một loại giá trị, trình biên dịch theo mặc định không tạo ra hàm tạo mặc định, cũng như không tạo cuộc gọi đến hàm tạo mặc định. Vì vậy, ngay cả khi bạn tình cờ xác định một hàm tạo mặc định, nó sẽ không được gọi và điều đó sẽ chỉ làm bạn bối rối.

Để tránh những vấn đề như vậy, trình biên dịch C # không cho phép định nghĩa của hàm tạo mặc định của người dùng. Và vì nó không tạo ra hàm tạo mặc định, bạn không thể khởi tạo các trường khi xác định chúng.

Hoặc lý do lớn là cấu trúc là một loại giá trị và các loại giá trị được khởi tạo bởi một giá trị mặc định và hàm tạo được sử dụng để khởi tạo.

Bạn không cần phải khởi tạo cấu trúc của mình bằng newtừ khóa. Nó thay vào đó hoạt động như một int; bạn có thể trực tiếp truy cập nó.

Cấu trúc không thể chứa các hàm tạo tham số rõ ràng. Các thành viên cấu trúc được tự động khởi tạo với các giá trị mặc định của chúng.

Một hàm tạo mặc định (không tham số) cho một cấu trúc có thể đặt các giá trị khác với trạng thái không có giá trị sẽ là hành vi không mong muốn. Do đó, thời gian chạy .NET cấm các hàm tạo mặc định cho một cấu trúc.


Câu trả lời này là tốt nhất. Cuối cùng, toàn bộ quan điểm hạn chế là để tránh những điều bất ngờ như MyStruct s;không gọi hàm tạo mặc định mà bạn cung cấp.
5/11/2015

1
Cám ơn vì đã giải thích. Vì vậy, nó chỉ là một trình biên dịch thiếu mà sẽ phải được cải thiện, không có lý do chính đáng nào về mặt lý thuyết để cấm các nhà xây dựng không tham số (ngay khi chúng có thể bị hạn chế chỉ truy cập các thuộc tính).
Elo

16

Bạn có thể tạo một thuộc tính tĩnh khởi tạo và trả về số "hợp lý" mặc định:

public static Rational One => new Rational(0, 1); 

Và sử dụng nó như:

var rat = Rational.One;

24
Trong trường hợp này, Rational.Zerocó thể là một chút ít nhầm lẫn.
Kevin

13

Giải thích ngắn hơn:

Trong C ++, struct và class chỉ là hai mặt của cùng một đồng tiền. Sự khác biệt thực sự duy nhất là một cái được công khai theo mặc định và cái kia là riêng tư.

Trong .NET , có một sự khác biệt lớn hơn nhiều giữa một cấu trúc và một lớp. Điều chính là struct cung cấp ngữ nghĩa loại giá trị, trong khi lớp cung cấp ngữ nghĩa loại tham chiếu. Khi bạn bắt đầu suy nghĩ về ý nghĩa của thay đổi này, các thay đổi khác cũng bắt đầu có ý nghĩa hơn, bao gồm cả hành vi của nhà xây dựng mà bạn mô tả.


7
Bạn sẽ phải nói rõ hơn một chút về cách điều này được ngụ ý bởi giá trị so với phân chia loại tham chiếu Tôi không hiểu được ...
Motti

Các loại giá trị có giá trị mặc định - chúng không phải là null, ngay cả khi bạn không xác định hàm tạo. Mặc dù thoạt nhìn, điều này không loại trừ việc xác định một hàm tạo mặc định, khung sử dụng tính năng này bên trong để đưa ra các giả định nhất định về các cấu trúc.
Joel Coehoorn

@annakata: Các nhà xây dựng khác có thể hữu ích trong một số tình huống liên quan đến Reflection. Ngoài ra, nếu thuốc generic được tăng cường để cho phép một ràng buộc "mới" được tham số hóa, sẽ rất hữu ích khi có các cấu trúc có thể tuân thủ chúng.
supercat

@annakata Tôi tin rằng đó là vì C # có một yêu cầu mạnh mẽ đặc biệt newphải thực sự được viết để gọi một nhà xây dựng. Trong C ++, các hàm tạo được gọi theo các cách ẩn, khi khai báo hoặc tạo ra các mảng. Trong C #, mọi thứ đều là con trỏ nên bắt đầu từ null, hoặc là cấu trúc và phải bắt đầu ở thứ gì đó, nhưng khi bạn không thể viết new... (như mảng init), điều đó sẽ phá vỡ quy tắc C # mạnh.
v.oddou

3

Tôi chưa thấy tương đương với giải pháp muộn mà tôi sẽ đưa ra, vì vậy đây là.

sử dụng offset để di chuyển các giá trị từ mặc định 0 vào bất kỳ giá trị nào bạn muốn. ở đây các thuộc tính phải được sử dụng thay vì truy cập trực tiếp vào các trường. (có thể với tính năng c # 7 có thể, bạn xác định rõ hơn các trường trong phạm vi thuộc tính để chúng vẫn được bảo vệ khỏi bị truy cập trực tiếp vào mã.)

Giải pháp này hoạt động cho các cấu trúc đơn giản chỉ có các loại giá trị (không có loại ref hoặc cấu trúc nullable).

public struct Tempo
{
    const double DefaultBpm = 120;
    private double _bpm; // this field must not be modified other than with its property.

    public double BeatsPerMinute
    {
        get => _bpm + DefaultBpm;
        set => _bpm = value - DefaultBpm;
    }
}

Điều này khác với câu trả lời này, cách tiếp cận này không phải là vỏ đặc biệt mà là sử dụng offset sẽ hoạt động cho tất cả các phạm vi.

ví dụ với enums là lĩnh vực.

public struct Difficaulty
{
    Easy,
    Medium,
    Hard
}

public struct Level
{
    const Difficaulty DefaultLevel = Difficaulty.Medium;
    private Difficaulty _level; // this field must not be modified other than with its property.

    public Difficaulty Difficaulty
    {
        get => _level + DefaultLevel;
        set => _level = value - DefaultLevel;
    }
}

Như tôi đã nói thủ thuật này có thể không hoạt động trong mọi trường hợp, ngay cả khi struct chỉ có các trường giá trị, chỉ bạn biết rằng nó có hoạt động trong trường hợp của bạn hay không. Chỉ cần kiểm tra. nhưng bạn có được ý tưởng chung.


Đây là một giải pháp tốt cho ví dụ tôi đã đưa ra nhưng nó thực sự chỉ được coi là một ví dụ, câu hỏi là chung chung.
Motti

2

Chỉ cần trường hợp đặc biệt thôi. Nếu bạn thấy tử số bằng 0 và mẫu số là 0, hãy giả vờ như nó có các giá trị bạn thực sự muốn.


5
Cá nhân tôi sẽ không thích các lớp học / cấu trúc của mình có loại hành vi này. Thất bại trong âm thầm (hoặc phục hồi theo cách mà các nhà phát triển đoán là tốt nhất cho bạn) là con đường dẫn đến những sai lầm chưa được giải quyết.
Boris Callens

2
+1 Đây là một câu trả lời hay, vì đối với các loại giá trị, bạn phải tính đến giá trị mặc định của chúng. Điều này cho phép bạn "đặt" giá trị mặc định với hành vi của nó.
IllidanS4 muốn Monica trở lại vào 16/07/2015

Đây chính xác là cách họ thực hiện các lớp như Nullable<T>(ví dụ int?).
Jonathan Allen

Đó là một ý tưởng rất tồi. 0/0 phải luôn là một phần không hợp lệ (NaN). Điều gì xảy ra nếu ai đó gọi new Rational(x,y)x và y là 0?
Mike Rosoft 23/07/19

Nếu bạn có một hàm tạo thực tế thì bạn có thể ném một ngoại lệ, ngăn không cho 0/0 thực sự xảy ra. Hoặc nếu bạn muốn nó xảy ra, bạn phải thêm một bool phụ để phân biệt giữa mặc định và 0/0.
Jonathan Allen

2

Những gì tôi sử dụng là toán tử hợp nhất null (??) kết hợp với trường sao lưu như thế này:

public struct SomeStruct {
  private SomeRefType m_MyRefVariableBackingField;

  public SomeRefType MyRefVariable {
    get { return m_MyRefVariableBackingField ?? (m_MyRefVariableBackingField = new SomeRefType()); }
  }
}

Hi vọng điêu nay co ich ;)

Lưu ý: việc gán kết hợp null hiện đang là đề xuất tính năng cho C # 8.0.


1

Bạn không thể xác định hàm tạo mặc định vì bạn đang sử dụng C #.

Structs có thể có các hàm tạo mặc định trong .NET, mặc dù tôi không biết bất kỳ ngôn ngữ cụ thể nào hỗ trợ nó.


Trong C #, các lớp và cấu trúc khác nhau về mặt ngữ nghĩa. Một struct là một loại giá trị, trong khi một lớp là một loại tham chiếu.
Tom Sarduy

-1

Đây là giải pháp của tôi cho vấn đề nan giải không có nhà xây dựng mặc định. Tôi biết đây là một giải pháp muộn, nhưng tôi nghĩ rằng đáng chú ý đây là một giải pháp.

public struct Point2D {
    public static Point2D NULL = new Point2D(-1,-1);
    private int[] Data;

    public int X {
        get {
            return this.Data[ 0 ];
        }
        set {
            try {
                this.Data[ 0 ] = value;
            } catch( Exception ) {
                this.Data = new int[ 2 ];
            } finally {
                this.Data[ 0 ] = value;
            }
        }
    }

    public int Z {
        get {
            return this.Data[ 1 ];
        }
        set {
            try {
                this.Data[ 1 ] = value;
            } catch( Exception ) {
                this.Data = new int[ 2 ];
            } finally {
                this.Data[ 1 ] = value;
            }
        }
    }

    public Point2D( int x , int z ) {
        this.Data = new int[ 2 ] { x , z };
    }

    public static Point2D operator +( Point2D A , Point2D B ) {
        return new Point2D( A.X + B.X , A.Z + B.Z );
    }

    public static Point2D operator -( Point2D A , Point2D B ) {
        return new Point2D( A.X - B.X , A.Z - B.Z );
    }

    public static Point2D operator *( Point2D A , int B ) {
        return new Point2D( B * A.X , B * A.Z );
    }

    public static Point2D operator *( int A , Point2D B ) {
        return new Point2D( A * B.Z , A * B.Z );
    }

    public override string ToString() {
        return string.Format( "({0},{1})" , this.X , this.Z );
    }
}

bỏ qua thực tế tôi có một cấu trúc tĩnh được gọi là null, (Lưu ý: Cái này chỉ dành cho tất cả góc phần tư dương), sử dụng get; set; trong C #, bạn có thể thử / bắt / cuối cùng, để xử lý các lỗi trong đó một loại dữ liệu cụ thể không được khởi tạo bởi hàm tạo mặc định Point2D (). Tôi đoán đây là một giải pháp cho một số người về câu trả lời này. Đó chủ yếu là lý do tại sao tôi thêm tôi. Sử dụng chức năng getter và setter trong C # sẽ cho phép bạn bỏ qua ý nghĩa của hàm tạo mặc định này và đặt thử bắt xung quanh những gì bạn chưa khởi tạo. Đối với tôi điều này hoạt động tốt, đối với người khác, bạn có thể muốn thêm một số câu lệnh if. Vì vậy, trong trường hợp bạn muốn thiết lập Số / Mẫu số, mã này có thể giúp ích. Tôi chỉ muốn nhắc lại rằng giải pháp này trông không đẹp, có thể còn hoạt động tồi tệ hơn từ quan điểm hiệu quả, nhưng, đối với ai đó đến từ phiên bản cũ hơn của C #, sử dụng kiểu dữ liệu mảng cung cấp cho bạn chức năng này. Nếu bạn chỉ muốn một cái gì đó hoạt động, hãy thử điều này:

public struct Rational {
    private long[] Data;

    public long Numerator {
        get {
            try {
                return this.Data[ 0 ];
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                return this.Data[ 0 ];
            }
        }
        set {
            try {
                this.Data[ 0 ] = value;
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                this.Data[ 0 ] = value;
            }
        }
    }

    public long Denominator {
        get {
            try {
                return this.Data[ 1 ];
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                return this.Data[ 1 ];
            }
        }
        set {
            try {
                this.Data[ 1 ] = value;
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                this.Data[ 1 ] = value;
            }
        }
    }

    public Rational( long num , long denom ) {
        this.Data = new long[ 2 ] { num , denom };
        /* Todo: Find GCD etc. */
    }

    public Rational( long num ) {
        this.Data = new long[ 2 ] { num , 1 };
        this.Numerator = num;
        this.Denominator = 1;
    }
}

2
Đây là mã rất xấu. Tại sao bạn có một tham chiếu mảng trong một cấu trúc? Tại sao bạn không đơn giản có tọa độ X và Y là các trường? Và sử dụng các ngoại lệ để kiểm soát dòng chảy là một ý tưởng tồi; bạn thường nên viết mã của mình theo cách mà NullReferenceException không bao giờ xảy ra. Nếu bạn thực sự cần điều này - mặc dù một cấu trúc như vậy sẽ phù hợp hơn cho một lớp hơn là một cấu trúc - thì bạn nên sử dụng khởi tạo lười biếng. (Và về mặt kỹ thuật, bạn - hoàn toàn không cần thiết trong mọi thiết lập ngoại trừ đầu tiên của tọa độ - cài đặt mỗi tọa độ hai lần.)
Mike Rosoft

-1
public struct Rational 
{
    private long numerator;
    private long denominator;

    public Rational(long num = 0, long denom = 1)   // This is allowed!!!
    {
        numerator   = num;
        denominator = denom;
    }
}

5
Nó được phép nhưng nó không được sử dụng khi không có tham số nào được chỉ định ideone.com/xsLloQ
Motti
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.