Java 또는 C #에서 다중 상속이 허용되지 않는 이유는 무엇입니까?


115

Java 및 C #에서는 다중 상속이 허용되지 않는다는 것을 알고 있습니다. 많은 책에서는 다중 상속이 허용되지 않는다고 말합니다. 그러나 인터페이스를 사용하여 구현할 수 있습니다. 허용되지 않는 이유에 대해서는 논의되지 않습니다. 아무도 그것이 허용되지 않는 이유를 정확하게 말할 수 있습니까?


1
바로이 점을 지적하는 같은 있다 가 어떤에서 C #을 수업에서 행동 MI-처럼 수 있도록 프레임 워크. 물론 상태 비 저장 믹스 인 (확장 메서드 사용)을 사용할 수있는 옵션이 있습니다. 상태 비 저장이기는하지만별로 유용하지 않습니다.
Dmitri Nesteruk

1
인터페이스가 상속과 관련이 있다는 것은 언어 구문에 의해 생성 된 환상입니다. 상속은 당신을 더 부자로 만들어줍니다. 인터페이스의 경우와 같은 것은 없습니다. 스쿼트를 상속하지 않고 훨씬 더 가난하게 만듭니다. 해리 삼촌의 도박 부채를 물려 받았으니 인터페이스를 구현하기 위해해야 ​​할 일이 더 많습니다.
Hans Passant

답변:


143

짧은 대답은 언어 디자이너가하지 않기로 결정했기 때문입니다.

기본적으로 .NET 및 Java 디자이너 모두 MI를 추가 하면 언어 에 너무 많은 복잡성추가 되는 반면 너무 적은 이점 을 제공 한다고 생각했기 때문에 다중 상속을 허용하지 않는 것 같습니다 .

좀 더 재미 있고 깊이있게 읽을 수 있도록 웹에서 일부 언어 디자이너의 인터뷰와 함께 사용할 수있는 기사가 있습니다. 예를 들어, .NET의 경우, CLR 관련 MS에서 일한 Chris Brumme는 다음과 같은 이유를 설명했습니다.

  1. 서로 다른 언어는 실제로 MI 작동 방식에 대해 서로 다른 기대치를 가지고 있습니다. 예를 들어, 충돌이 해결되는 방법과 중복 기반이 병합 또는 중복되는지 여부가 있습니다. CLR에서 MI를 구현하기 전에 모든 언어를 조사하고 공통 개념을 파악하고 언어 중립적 인 방식으로 표현하는 방법을 결정해야합니다. 또한 MI가 CLS에 속하는지 여부와이 개념을 원하지 않는 언어 (예 : VB.NET)에서 이것이 의미하는 바를 결정해야합니다. 물론 이것이 우리가 공통 언어 런타임으로하고있는 사업이지만 아직 MI를 위해 수행하지는 않았습니다.

  2. MI가 진정으로 적절한 곳의 수는 실제로 매우 적습니다. 대부분의 경우 다중 인터페이스 상속이 대신 작업을 수행 할 수 있습니다. 다른 경우에는 캡슐화 및 위임을 사용할 수 있습니다. 믹스 인과 같은 약간 다른 구조를 추가하면 실제로 더 강력할까요?

  3. 다중 구현 상속은 구현에 많은 복잡성을 주입합니다. 이러한 복잡성은 캐스팅, 레이아웃, 디스패치, 필드 액세스, 직렬화, ID 비교, 검증 가능성, 반사, 제네릭 및 기타 여러 장소에 영향을 미칩니다.

여기에서 전체 기사를 읽을 수 있습니다.

Java의 경우 다음 기사를 읽을 수 있습니다 .

Java 언어에서 다중 상속을 생략하는 이유는 대부분 "단순하고 객체 지향적이며 친숙한"목표에서 비롯됩니다. 단순한 언어로서 Java의 제작자는 대부분의 개발자가 광범위한 교육없이 이해할 수있는 언어를 원했습니다. 이를 위해 그들은 C ++의 불필요한 복잡성 (단순함)을 넘기지 않고 가능한 한 C ++와 유사한 언어 (익숙한)를 만들기 위해 노력했습니다.

디자이너의 의견으로는 다중 상속은 해결하는 것보다 더 많은 문제와 혼란을 야기합니다. 그래서 그들은 (연산자 과부하를 줄이는 것처럼) 언어에서 다중 상속을 자릅니다. 디자이너의 광범위한 C ++ 경험을 통해 다중 상속은 골칫거리가 아니라는 사실을 알게되었습니다.


10
좋은 비교. 제가 생각하는 두 플랫폼의 기초가되는 사고 과정을 꽤 잘 나타냅니다.
CurtainDog 2009-06-15

97

구현의 다중 상속은 허용되지 않습니다.

문제는 컴파일러 / 런타임이 Draw () 메서드에 대한 구현이있는 Cowboy 및 Artist 클래스가있는 경우 수행 할 작업을 파악할 수 없다는 것입니다. 그런 다음 새 CowboyArtist 형식을 만들려고합니다. draw () 메서드를 호출하면 어떻게됩니까? 누군가가 거리에서 죽어 있나요, 아니면 아름다운 수채화가 있나요?

이중 다이아몬드 상속 문제라고 생각합니다.


80
당신은 거리에서 죽은 누군가의 아름다운 수채화를 얻습니다 :-)
Dan F

이것이 유일한 문제입니까? C ++가 가상 키워드로이 문제를 해결하는지 잘 모르겠습니다. 이것이 사실입니까? 나는 C ++를 잘 못합니다
Abdulsattar Mohammed

2
C ++에서는 파생에서 호출 할 기본 클래스 함수를 지정하거나 직접 다시 구현할 수 있습니다.
Dmitry Risenberg

1
현대적인 디자인은 가장 단순한 경우를 제외한 모든 경우에 상속보다 구성을 선호하는 경향이 있습니다. 다중 상속은 단순한 경우로 간주되지 않았습니다. 컴포지션을 사용하면 이와 같은 다이아몬드 문제가있을 때 클래스가 수행하는 작업을 정확하게 제어 할 수 있습니다.
Bill Michell 2009-06-15

우선 순위 목록 만 있으면됩니다. Common Lisp의 객체 시스템 인 CLOS에서 사용됩니다. 모든 클래스는 heirachy를 형성하고 호출되는 메서드는 거기에서 결정됩니다. 또한 CLOS를 사용하면 CowboyArtist.draw ()가 먼저 카우보이를 그린 다음 아티스트를 그릴 수 있도록 다른 규칙을 정의 할 수 있습니다. 또는 무엇을 구성하든.
Sebastian Krog

19

이유 : Java는 단순성 때문에 매우 인기 있고 코딩하기 쉽습니다.

그래서 자바 개발자들은 프로그래머가 이해하기 어렵고 복잡하다고 느끼는 것을 피하려고 노력했습니다. 이러한 종류의 속성 중 하나는 다중 상속입니다.

  1. 그들은 포인터를 피했습니다.
  2. 그들은 다중 상속을 피했습니다.

다중 상속 문제 : 다이아몬드 문제.

:

  1. 클래스 A에 fun () 메서드가 있다고 가정합니다. 클래스 B와 클래스 C는 클래스 A에서 파생됩니다.
  2. 클래스 B와 C는 모두 fun () 메서드를 재정의합니다.
  3. 이제 클래스 D가 클래스 B와 C를 모두 상속한다고 가정합니다 (가정 만 해당).
  4. 클래스 D에 대한 개체를 만듭니다.
  5. D d = 새로운 D ();
  6. d.fun (); 액세스를 시도하십시오. => 클래스 B의 fun () 또는 클래스 C의 fun ()을 호출할까요?

이것은 다이아몬드 문제에 존재하는 모호성입니다.

이 문제를 해결하는 것은 불가능하지 않지만, 읽는 동안 프로그래머에게 더 많은 혼란과 복잡성을 야기합니다. 해결하려고하는 것보다 더 많은 문제를 일으 킵니다.

참고 : 그러나 어떤 방법 으로든 인터페이스를 사용하여 항상 간접적으로 다중 상속을 구현할 수 있습니다.


1
"그러나 어떤 식 으로든 인터페이스를 사용하여 항상 간접적으로 다중 상속을 구현할 수 있습니다." -프로그래머가 매번 반복해서 메소드의 본문을 다시 지정해야합니다.
Kari

13

Java는 C ++와 크게 다른 디자인 철학을 가지고 있기 때문입니다. (여기서 C #은 논의하지 않겠습니다.)

C ++를 설계 할 때 Stroustrup은 어떻게 오용 될 수 있는지에 관계없이 유용한 기능을 포함하기를 원했습니다. 다중 상속, 연산자 오버로딩, 템플릿 및 기타 다양한 기능을 사용하여 큰 시간을 낭비 할 수 있지만이를 사용하여 아주 좋은 작업을 수행 할 수도 있습니다.

Java 디자인 철학은 언어 구조의 안전성을 강조하는 것입니다. 그 결과 훨씬 더 어색한 일이 있지만,보고있는 코드가 자신이 생각하는 일을 의미한다는 사실을 훨씬 더 확신 할 수 있습니다.

게다가 자바는 가장 잘 알려진 OO 언어 인 C ++와 스몰 토크의 반응이었다. MI를 더 잘 처리하는 다른 OO 시스템과 함께 많은 다른 OO 언어 (Common Lisp가 실제로 표준화 된 첫 번째 언어)가 있습니다.

인터페이스, 구성 및 위임을 사용하여 Java에서 MI를 수행하는 것이 전적으로 가능하다는 것은 말할 것도 없습니다. C ++보다 더 명시 적이므로 사용하기 어색하지만 언뜻보기에 이해하기 쉬운 내용을 얻을 수 있습니다.

여기에는 정답이 없습니다. 다양한 답변이 있으며 주어진 상황에 대해 어느 것이 더 나은지는 응용 프로그램과 개인의 선호도에 따라 다릅니다.


12

사람들이 MI에서 멀어지는 주된 이유는 (유일한 것은 아니지만) 구현의 모호성을 유발하는 소위 "다이아몬드 문제"입니다. 이 위키피디아 기사 는 그것에 대해 논의하고 내가 할 수있는 것보다 더 잘 설명합니다. MI는 또한 더 복잡한 코드로 이어질 수 있으며 많은 OO 디자이너는 MI가 필요하지 않다고 주장하며 MI를 사용하면 모델이 잘못되었을 수 있습니다. 이 마지막 요점에 동의하는지 잘 모르겠지만 단순하게 유지하는 것은 항상 좋은 계획입니다.


8

C ++에서 다중 상속은 부적절하게 사용될 때 큰 골칫거리였습니다. 이러한 인기있는 디자인 문제를 피하기 위해 대신 현대 언어 (java, C #)에서 여러 인터페이스 "상속"이 강제되었습니다.


8

다중 상속은

  • 이해하기 어렵다
  • 디버그하기 어려움 (예를 들어, 동일한 이름의 메서드가있는 여러 프레임 워크의 클래스를 깊게 혼합하면 예상치 못한 시너지가 발생할 수 있음)
  • 오용하기 쉽다
  • 정말로 유용
  • 특히 정확 하고 효율적으로 수행하려는 경우 구현하기가 어렵습니다.

따라서 Java 언어에 다중 상속을 포함 하지 않는 것이 현명한 선택으로 간주 될 수 있습니다 .


3
나는 Common Lisp Object System으로 작업 한 위의 모든 것에 동의하지 않습니다.
David Thornley

2
동적 언어는 중요하지 않습니다. ;-) Lisp와 같은 시스템에서는 디버깅을 다소 쉽게 만드는 REPL이 있습니다. 또한, CLOS (내가 이해했듯이, 나는 그것을 실제로 사용한 적이없고 그것에 대해서만 읽었다)는 많은 유연성과 자신 만의 태도를 가진 메타 오브젝트 시스템이다. 그러나 컴파일러가 여러 개의 (겹칠 가능성이있는) vtable을 사용하여 엄청나게 복잡한 메서드 조회를 생성하는 C ++와 같은 정적으로 컴파일 된 언어를 생각해보십시오. 이러한 구현에서 호출 된 메서드 구현을 찾는 것은 그리 사소한 작업이 아닐 수 있습니다.
MFX

5

또 다른 이유는 단일 상속으로 인해 캐스팅이 사소하게 만들어져 어셈블러 명령어를 생성하지 않기 때문입니다 (필요한 경우 유형의 호환성을 확인하는 것 제외). 다중 상속이있는 경우 자식 클래스에서 특정 부모가 시작되는 위치를 파악해야합니다. 따라서 성능은 확실히 특전입니다 (유일한 것은 아니지만).


4

컴퓨터 과학이 과학에 가깝고 대량 생산이 적었던 옛날 (70 년대) 프로그래머는 좋은 디자인과 좋은 구현에 대해 생각할 시간을 가졌고 그 결과 제품 (프로그램)이 고품질 (예 : TCP / IP 디자인)을 가졌습니다. 및 구현). 요즘은 모두가 프로그래밍하고 관리자가 마감일 전에 사양을 변경하고있을 때 Steve Haigh 게시물의 위키피디아 링크에 설명 된 것과 같은 미묘한 문제를 추적하기가 어렵습니다. 따라서 "다중 상속"은 컴파일러 설계에 의해 제한됩니다. 당신이 그것을 좋아한다면, 당신은 여전히 ​​C ++를 사용할 수 있습니다 .... 그리고 당신이 원하는 모든 자유를 가질 수 있습니다 :)


4
... 발에 자신을 여러 번 쏠 수있는 자유를 포함하여;)
Mario Ortegón 2009-06-15

4

나는 "자바에서는 다중 상속이 허용되지 않는다"는 말을 조금만 들었다.

다중 상속은 "유형"이 둘 이상의 "유형"에서 상속 될 때 정의됩니다. 또한 인터페이스는 동작이 있으므로 유형으로 분류됩니다. 따라서 Java에는 다중 상속이 있습니다. 더 안전합니다.


6
그러나 인터페이스에서 상속하지 않고 구현합니다. 인터페이스는 클래스가 아닙니다.
Blorgbeard는

2
예, 그러나 일부 예비 책을 제외하고 상속은 그렇게 좁게 정의되지 않았습니다. 그리고 저는 우리가 그것의 문법을 넘어서야한다고 생각합니다. 둘 다 같은 일을하고 인터페이스는 그 이유만으로 "발명"되었습니다.
Rig Veda

Closeable 인터페이스 : 하나의 메서드, close ()를 사용합니다. 이를 Openable로 확장하고 싶다고 가정합니다. 부울 필드 isOpen을 true로 설정하는 open () 메소드가 있습니다. 열려 있지 않은 Openable을 닫으려는 것과 마찬가지로 이미 열려있는 Openable을 열려고하면 예외가 발생합니다. Openable 클래스의 시간. Openable이 단순히 인터페이스라면이 기능을 제공 할 수 없습니다. QED : 인터페이스는 상속을 제공하지 않습니다!
마이크는 설치류

4

클래스의 동적로드는 다중 상속 구현을 어렵게 만듭니다.

Java에서는 실제로 단일 상속과 인터페이스를 사용하여 다중 상속의 복잡성을 피했습니다. 다음과 같은 상황에서 다중 상속의 복잡성이 매우 높습니다.

다중 상속의 다이아몬드 문제. A에서 상속 된 두 개의 클래스 B와 C가 있습니다. B와 C가 상속 된 메서드를 재정의하고 자체 구현을 제공한다고 가정합니다. 이제 D는 B와 C 모두에서 다중 상속을 수행합니다. D는 재정의 된 메서드를 상속해야합니다. jvm이 어떤 재정의 된 메서드를 사용할지 결정할 수 없습니까?

C ++에서 가상 함수는 처리하는 데 사용되며 명시 적으로 수행해야합니다.

이것은 인터페이스를 사용하여 피할 수 있으며 메서드 본문이 없습니다. 인터페이스는 인스턴스화 할 수 없으며 클래스로만 구현하거나 다른 인터페이스로 확장 할 수 있습니다.


3

실제로 상속 된 클래스의 기능이 동일한 경우 다중 상속이 복잡해집니다. 즉, 컴파일러는 선택해야하는 혼란을 갖게됩니다 (다이아몬드 문제). 따라서 Java에서는 복잡성이 제거되고 다중 상속과 같은 기능을 얻을 수있는 인터페이스가 제공되었습니다. 인터페이스를 사용할 수 있습니다


2

자바에는 개념, 즉 다형성이 있습니다. Java에는 두 가지 유형의 다형성이 있습니다. 메서드 오버로딩과 메서드 재정의가 있습니다. 그중 메서드 재정의는 수퍼 클래스 및 하위 클래스 관계에서 발생합니다. 서브 클래스의 객체를 생성하고 수퍼 클래스의 메소드를 호출하고 서브 클래스가 둘 이상의 클래스를 확장하는 경우 어떤 수퍼 클래스 메소드를 호출해야합니까?

또는으로 슈퍼 클래스 생성자를 호출하는 동안 super()어떤 슈퍼 클래스 생성자가 호출됩니까?

이 결정은 현재 Java API 기능으로 불가능합니다. 따라서 Java에서는 다중 상속이 허용되지 않습니다.


기본적으로 Java의 유형 시스템은 모든 객체 인스턴스에 하나의 유형이 있다고 가정하고 객체를 상위 유형으로 캐스트하면 항상 작동하고 참조 보존되며, 인스턴스가 해당 하위 유형이거나 해당 인스턴스 인 경우 하위 유형에 대한 참조를 캐스팅하면 참조 보존이됩니다. 그 하위 유형. 이러한 가정은 유용하며 그와 일치하는 방식으로 일반화 된 다중 상속을 허용하는 것이 가능하지 않다고 생각합니다.
supercat

1

다중 상속은 Java에서 직접 허용되지 않지만 인터페이스를 통해 허용됩니다.

이유 :

다중 상속 : 더 복잡하고 모호함을 도입합니다.

인터페이스 : 인터페이스는 공개적으로 사용 가능한 인터페이스에서 프로그램의 구조 또는 내부 작업을 적절하게 설명하는 일관된 방법을 제공하는 Java의 완전히 추상적 인 클래스이며, 그 결과 더 많은 유연성과 재사용 가능한 코드 및 더 많은 제어가 가능합니다. 다른 클래스를 만들고 상호 작용하는 방법에 대해

좀 더 정확하게 말하자면, 그것들은 하나 이상의 클래스로 업 캐스트 될 수있는 클래스들과 같이 일종의 다중 상속을 수행 할 수있게 해주는 추가 특성을 가진 Java의 특별한 구조입니다.

간단한 예를 들어 보겠습니다.

  1. 메서드 이름은 같지만 기능이 다른 두 개의 슈퍼 클래스 클래스 A와 B가 있다고 가정합니다. (확장) 키워드로 다음 코드를 통해 다중 상속이 불가능합니다.

       public class A                               
         {
           void display()
             {
               System.out.println("Hello 'A' ");
             }
         }
    
       public class B                               
          {
            void display()
              {
                System.out.println("Hello 'B' ");
              }
          }
    
      public class C extends A, B    // which is not possible in java
        {
          public static void main(String args[])
            {
              C object = new C();
              object.display();  // Here there is confusion,which display() to call, method from A class or B class
            }
        }
  2. 그러나 인터페이스를 통해 키워드로 (구현) 다중 상속이 가능합니다.

    interface A
        {
           // display()
        }
    
    
     interface B
        {
          //display()
        }
    
     class C implements A,B
        {
           //main()
           C object = new C();
           (A)object.display();     // call A's display
    
           (B)object.display(); //call B's display
        }
    }

0

아무도 그것이 허용되지 않는 이유를 정확하게 말할 수 있습니까?

이 문서 링크 에서 답변을 찾을 수 있습니다.

Java 프로그래밍 언어가 둘 이상의 클래스를 확장 할 수없는 한 가지 이유는 여러 클래스에서 필드를 상속하는 기능인 상태의 다중 상속 문제를 방지하기위한 것입니다.

다중 상속이 허용되고 해당 클래스를 인스턴스화하여 개체를 만들 때 해당 개체는 모든 클래스의 슈퍼 클래스에서 필드를 상속합니다. 두 가지 문제가 발생합니다.

  1. 다른 슈퍼 클래스의 메서드 나 생성자가 동일한 필드를 인스턴스화하면 어떻게됩니까?

  2. 어떤 메서드 또는 생성자가 우선할까요?

이제 상태의 다중 상속이 허용되지만 여전히 구현할 수 있습니다.

유형의 다중 상속 : 클래스가 둘 이상의 인터페이스를 구현하는 능력.

구현의 다중 상속 (인터페이스의 기본 메서드를 통해) : 여러 클래스에서 메서드 정의를 상속하는 기능

추가 정보는이 관련 SE 질문을 참조하십시오.

인터페이스가있는 다중 상속 모호성


0

C ++에서 클래스는 둘 이상의 클래스에서 직접 또는 간접적으로 상속 할 수 있습니다 .이를 다중 상속 이라고합니다 .

그러나 C # 및 Java 는 각 클래스가 단일 부모 클래스에서 상속 하는 단일 상속으로 클래스를 제한 합니다.

다중 상속은 단일 애플리케이션 내에서 서로 다른 클래스 프레임 워크를 사용할 때 종종 발생하는 두 개의 서로 다른 클래스 계층의 측면을 결합하는 클래스를 만드는 유용한 방법입니다.

예를 들어 두 개의 프레임 워크가 예외에 대한 자체 기본 클래스를 정의하는 경우 다중 상속을 사용하여 두 프레임 워크에서 사용할 수있는 예외 클래스를 만들 수 있습니다.

다중 상속의 문제점은 모호함으로 이어질 수 있다는 것입니다. 고전적인 예는 한 클래스가 다른 두 클래스에서 상속되는 경우이며 각 클래스는 동일한 클래스에서 상속됩니다.

class A {
    protected:
    bool flag;
};
class B : public A {};
class C : public A {};
class D : public B, public C {
    public:
    void setFlag( bool nflag ){
        flag = nflag; // ambiguous
    }
};

이 예에서 flag데이터 멤버는에 의해 정의됩니다 class A. 그러나 둘 다에서 파생 class D되는 class B 및의 자손 이므로의 두 인스턴스가 의 클래스 계층 구조 에 있기 때문에 본질적으로 두 개의 복사본 을 사용할 수 있습니다 . 어느 것을 설정 하시겠습니까? 컴파일러는 in에 대한 참조 가 모호 하다고 불평 합니다 . 한 가지 수정 사항은 참조를 명시 적으로 명확하게하는 것입니다.class CAflagADflagD

B::flag = nflag;

또 다른 수정 사항은 B와 C를로 선언 virtual base classes하는 것입니다. 즉, A의 복사본 하나만 계층 구조에 존재할 수 있으므로 모호함이 제거됩니다.

파생 개체가 생성 될 때 기본 클래스가 초기화되는 순서 또는 파생 클래스에서 멤버를 실수로 숨길 수있는 방법과 같이 다중 상속에는 다른 복잡성이 있습니다. 이러한 복잡성을 피하기 위해 일부 언어는 더 단순한 단일 상속 모델로 제한됩니다.

이것은 상속을 상당히 단순화하지만 공통 조상을 가진 클래스 만 동작을 공유 할 수 있기 때문에 유용성을 제한합니다. 인터페이스는 서로 다른 계층 구조의 클래스가 코드를 공유하여 구현되지 않더라도 공통 인터페이스를 노출하도록 허용하여 이러한 제한을 다소 완화합니다.


-1

이 예를 상상해보십시오 : 수업이 있습니다 Shape1

그것은이 CalcualteArea방법을 :

Class Shape1
{

 public void CalculateArea()

     {
       //
     }
}

Shape2동일한 방법을 가진 또 다른 클래스 가 있습니다.

Class Shape2
{

 public void CalculateArea()

     {

     }
}

이제 Circle 자식 클래스가 있는데 Shape1과 Shape2에서 파생됩니다.

public class Circle: Shape1, Shape2
{
}

이제 Circle에 대한 객체를 생성하고 메서드를 호출 할 때 시스템이 호출 할 영역 계산 메서드를 알지 못합니다. 둘 다 동일한 서명이 있습니다. 따라서 컴파일러는 혼란 스러울 것입니다. 이것이 다중 상속이 허용되지 않는 이유입니다.

그러나 인터페이스에는 메서드 정의가 없기 때문에 여러 인터페이스가있을 수 있습니다. 두 인터페이스 모두 동일한 메서드를 가지고 있어도 둘 다 구현이 없으며 항상 자식 클래스의 메서드가 실행됩니다.


언어 디자이너는 인터페이스 에서처럼 명시적인 메서드 호출을 제공 할 수 있습니다. 그런 요점이 없습니다! 또 다른 이유는 추상 메서드를 사용하는 추상 클래스는 어떨까요?
Nomi Ali
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.