Dlaczego dziedziczenie wielokrotne nie jest dozwolone w języku Java lub C #?

115

Wiem, że dziedziczenie wielokrotne nie jest dozwolone w Javie i C #. Wiele książek po prostu mówi, że wielokrotne dziedziczenie jest niedozwolone. Ale można to zaimplementować za pomocą interfejsów. Nic nie jest omawiane na temat tego, dlaczego jest to niedozwolone. Czy ktoś może mi dokładnie powiedzieć, dlaczego jest to zabronione?

Abdulsattar Mohammed
źródło
1
Wystarczy przypomnieć, że tam ramy, które obecnie nie pozwalają MI-jak zachowanie w C # klas. I oczywiście masz możliwość posiadania bezstanowych miksów (przy użyciu metod rozszerzeń) - będąc bezstanowymi, nie są one jednak zbyt przydatne.
Dmitri Nesteruk
1
To, że interfejsy mają cokolwiek wspólnego z dziedziczeniem, jest iluzją stworzoną przez składnię języka. Dziedziczenie ma uczynić cię bogatszym. Nic takiego w przypadku interfejsu, nie dziedziczysz przysiadu, a to sprawia, że ​​jesteś znacznie biedniejszy. Odziedziczyłeś hazardowy dług wujka Harry'ego, masz więcej do zrobienia, aby zaimplementować interfejs.
Hans Passant

Odpowiedzi:

143

Krótka odpowiedź brzmi: ponieważ projektanci języka zdecydowali się tego nie robić.

Zasadniczo wydawało się, że zarówno projektanci .NET, jak i Java nie zezwalali na wielokrotne dziedziczenie, ponieważ uważali, że dodanie MI spowodowało zbyt dużą złożoność języków, a jednocześnie zapewniało zbyt małe korzyści .

Aby uzyskać ciekawszą i bardziej dogłębną lekturę, w Internecie dostępnych jest kilka artykułów zawierających wywiady z niektórymi projektantami języka. Na przykład w przypadku .NET Chris Brumme (który pracował w MS nad CLR) wyjaśnił powody, dla których zdecydowali się nie:

  1. Różne języki mają różne oczekiwania co do sposobu działania MI. Na przykład, w jaki sposób rozwiązywane są konflikty i czy zduplikowane bazy są scalane czy nadmiarowe. Zanim będziemy mogli zaimplementować MI w CLR, musimy przeprowadzić przegląd wszystkich języków, znaleźć wspólne koncepcje i zdecydować, jak je wyrazić w sposób neutralny językowo. Musielibyśmy również zdecydować, czy MI należy do CLS i co to oznaczałoby dla języków, które nie chcą tego pojęcia (na przykład VB.NET). Oczywiście jest to biznes, w którym pracujemy jako środowisko wykonawcze języka wspólnego, ale nie udało nam się jeszcze tego zrobić dla MI.

  2. Liczba miejsc, w których MI jest naprawdę odpowiednia, jest w rzeczywistości niewielka. W wielu przypadkach można zamiast tego wykonać dziedziczenie wielu interfejsów. W innych przypadkach możesz użyć hermetyzacji i delegowania. Gdybyśmy dodali nieco inną konstrukcję, taką jak miksy, czy faktycznie byłaby ona potężniejsza?

  3. Dziedziczenie wielu implementacji wprowadza dużą złożoność do implementacji. Ta złożoność wpływa na odlewanie, układ, wysyłkę, dostęp do pola, serializację, porównania tożsamości, weryfikowalność, refleksję, typy ogólne i prawdopodobnie wiele innych miejsc.

Tutaj możesz przeczytać pełny artykuł.

W przypadku języka Java możesz przeczytać ten artykuł :

Powody pominięcia wielokrotnego dziedziczenia z języka Java wynikają głównie z „prostego, obiektowego i znajomego” celu. Twórcy Javy jako prostego języka chcieli języka, który większość programistów mogłaby zrozumieć bez intensywnego szkolenia. W tym celu pracowali nad tym, aby język był jak najbardziej podobny do C ++ (znajomy) bez przenoszenia niepotrzebnej złożoności C ++ (proste).

Zdaniem projektantów dziedziczenie wielokrotne powoduje więcej problemów i zamieszania niż rozwiązuje. Więc wycinają wielokrotne dziedziczenie z języka (tak samo, jak zmniejszają przeciążenie operatora). Bogate doświadczenie projektantów w C ++ nauczyło ich, że wielokrotne dziedziczenie po prostu nie jest warte bólu głowy.

Razzie
źródło
10
Niezłe porównanie. Wydaje mi się, że dość wskazuje na procesy myślowe, które leżą u podstaw obu platform.
CurtainDog
97

Wielokrotne dziedziczenie implementacji jest niedozwolone.

Problem polega na tym, że kompilator / środowisko wykonawcze nie może dowiedzieć się, co zrobić, jeśli masz klasę Cowboy i Artist, obie z implementacjami metody draw (), a następnie próbujesz utworzyć nowy typ CowboyArtist. Co się dzieje, gdy wywołujesz metodę draw ()? Czy ktoś leży martwy na ulicy, czy masz śliczną akwarelę?

Uważam, że nazywa się to problemem dziedziczenia podwójnego diamentu.

duffymo
źródło
80
Dostajesz cudowną akwarelę przedstawiającą kogoś martwego na ulicy :-)
Dan F,
Czy to jedyny problem? Myślę, że nie jestem pewien, czy C ++ rozwiązuje ten problem za pomocą wirtualnego słowa kluczowego, czy to prawda? Nie jestem dobry w C ++
Abdulsattar Mohammed
2
W C ++ możesz albo określić, które funkcje klasy bazowej mają być wywoływane w pochodnych, albo ponownie zaimplementować je samodzielnie.
Dmitry Risenberg
1
Współczesny projekt ma tendencję do faworyzowania kompozycji nad dziedziczeniem we wszystkich przypadkach oprócz najprostszych. Dziedziczenie wielokrotne nigdy nie liczyłoby się jako prosty przypadek. Dzięki kompozycji masz precyzyjną kontrolę nad tym, co robi twoja klasa, gdy występują problemy z diamentami, takie jak ten ...
Bill Michell
Potrzebowałbyś po prostu listy priorytetów. Jest to używane w systemie obiektowym Common Lisp, CLOS. Wszystkie klasy tworzą dziedziczenie i stąd określana jest wywoływana metoda. Ponadto CLOS umożliwia zdefiniowanie różnych reguł, tak że CowboyArtist.draw () może najpierw narysować Cowboya, a następnie Artist. Albo cokolwiek wymyślisz.
Sebastian Krog
19

Powód: Java jest bardzo popularna i łatwa do kodowania ze względu na swoją prostotę.

Więc to, co kiedykolwiek programiści Java wydają się trudne do zrozumienia dla programistów, starali się tego uniknąć. Jednym z takich rodzajów własności jest dziedziczenie wielokrotne.

  1. Unikali wskazówek
  2. Uniknęli wielokrotnego dziedziczenia.

Problem z dziedziczeniem wielokrotnym: problem z diamentami.

Przykład :

  1. Załóżmy, że klasa A ma metodę fun (). klasa B i klasa C wywodzi się z klasy A.
  2. Obie klasy B i C przesłaniają metodę fun ().
  3. Teraz załóżmy, że klasa D dziedziczy zarówno klasę B, jak i C. (po prostu Założenie)
  4. Utwórz obiekt dla klasy D.
  5. D d = nowy D ();
  6. i spróbuj uzyskać dostęp do d.fun (); => czy wywoła fun () klasy B czy fun () klasy C?

To jest dwuznaczność istniejąca w problemie diamentów.

Nie jest niemożliwe rozwiązanie tego problemu, ale powoduje to większe zamieszanie i złożoność dla programisty podczas czytania. Powoduje więcej problemów niż próbuje rozwiązać.

Uwaga : ale zawsze możesz pośrednio zaimplementować wielokrotne dziedziczenie za pomocą interfejsów.

user1923551
źródło
1
„Ale w każdy sposób można zawsze pośrednio zaimplementować wielokrotne dziedziczenie za pomocą interfejsów”. - co wymaga od programisty ponownego określania treści metod za każdym razem.
Kari
13

Ponieważ Java ma znacznie inną filozofię projektowania niż C ++. (Nie będę tutaj omawiać języka C #).

Projektując C ++, Stroustrup chciał uwzględnić przydatne funkcje, niezależnie od tego, w jaki sposób można je niewłaściwie wykorzystać. Możliwe jest zepsucie wielu rzeczy z wielokrotnym dziedziczeniem, przeciążeniem operatorów, szablonami i różnymi innymi funkcjami, ale można też zrobić z nimi kilka bardzo dobrych rzeczy.

Filozofia projektowania w Javie polega na podkreśleniu bezpieczeństwa w konstrukcjach językowych. W rezultacie istnieją rzeczy, które są o wiele trudniejsze do wykonania, ale możesz być dużo bardziej pewny, że kod, na który patrzysz, oznacza to, co myślisz, że robi.

Co więcej, Java była w dużej mierze reakcją C ++ i Smalltalk, najbardziej znanych języków obiektowych. Istnieje wiele innych języków obiektowych (Common Lisp był właściwie pierwszym znormalizowanym językiem), z różnymi systemami obiektowymi, które lepiej obsługują MI.

Nie wspominając już o tym, że jest całkowicie możliwe wykonanie MI w Javie, używając interfejsów, kompozycji i delegowania. Jest bardziej wyraźny niż w C ++ i dlatego jest trudniejszy w użyciu, ale da ci coś, co prawdopodobnie zrozumiesz na pierwszy rzut oka.

Nie ma tutaj właściwej odpowiedzi. Odpowiedzi są różne, a to, która z nich jest lepsza w danej sytuacji, zależy od aplikacji i indywidualnych preferencji.

David Thornley
źródło
12

Głównym (choć nie jedynym) powodem, dla którego ludzie odwracają się od MI, jest tak zwany „problem diamentów” prowadzący do niejasności w implementacji. Ten artykuł na Wikipedii omawia to i wyjaśnia lepiej niż ja. MI może również prowadzić do bardziej złożonego kodu, a wielu projektantów OO twierdzi, że nie potrzebujesz MI, a jeśli go używasz, prawdopodobnie twój model jest zły. Nie jestem pewien, czy zgadzam się z tym ostatnim punktem, ale prostota jest zawsze dobrym planem.

Steve
źródło
8

W C ++ wielokrotne dziedziczenie było głównym problemem, gdy zostało użyte nieprawidłowo. Aby uniknąć tych popularnych problemów projektowych, we współczesnych językach (java, C #) wymuszono „dziedziczenie” wielu interfejsów.

inazaruk
źródło
8

Dziedziczenie wielokrotne to

  • ciężko zrozumieć
  • trudne do debugowania (na przykład, jeśli mieszasz klasy z wielu frameworków, które mają w głębi te same metody, mogą wystąpić dość nieoczekiwane synergie)
  • łatwy w użyciu
  • naprawdę nie że użyteczny
  • trudne do wdrożenia, zwłaszcza jeśli chcesz, aby zostało wykonane poprawnie i wydajnie

Dlatego rozsądnym wyborem może być nie uwzględnianie wielokrotnego dziedziczenia w języku Java.

mfx
źródło
3
Nie zgadzam się z powyższym, pracując z Common Lisp Object System.
David Thornley
2
Języki dynamiczne się nie liczą ;-) W każdym systemie podobnym do Lispa masz REPL, który sprawia, że ​​debugowanie jest dość łatwe. Ponadto CLOS (jak rozumiem, nigdy go tak naprawdę nie używałem, tylko o nim czytałem) jest systemem meta-obiektowym, z dużą elastycznością i nastawieniem na własne podejście. Ale weźmy pod uwagę statycznie skompilowany język, taki jak C ++, w którym kompilator generuje diabelsko złożone wyszukiwanie metod przy użyciu wielu (prawdopodobnie nakładających się) tabel vtables: w takiej implementacji ustalenie, jaka implementacja metody została wywołana, może nie być tak banalnym zadaniem.
mfx
5

Innym powodem jest to, że dziedziczenie pojedyncze sprawia, że ​​rzutowanie staje się trywialne, nie emitując instrukcji asemblera (poza sprawdzaniem zgodności typów, jeśli jest to wymagane). Gdybyś miał dziedziczenie wielokrotne, musiałbyś dowiedzieć się, gdzie w klasie podrzędnej zaczyna się dany rodzic. Zatem wydajność jest z pewnością zaletą (choć nie jedyną).

Blindy
źródło
4

W dawnych czasach (lata 70.), kiedy informatyka była bardziej naukowa i mniej masowa, programiści mieli czas na zastanowienie się nad dobrym projektem i dobrym wdrożeniem, w wyniku czego produkty (programy) charakteryzowały się wysoką jakością (np. Projekt TCP / IP) i wdrożenie). W dzisiejszych czasach, kiedy wszyscy programują, a menedżerowie zmieniają specyfikacje przed terminami, subtelne kwestie, takie jak te opisane w linku do Wikipedii z postu Steve'a Haigha, są trudne do śledzenia; dlatego „dziedziczenie wielokrotne” jest ograniczone konstrukcją kompilatora. Jeśli Ci się spodoba, nadal możesz używać C ++ ... i mieć pełną swobodę :)


źródło
4
... łącznie z możliwością wielokrotnego strzelania sobie w stopę;)
Mario Ortegón
4

Stwierdzenie, że „dziedziczenie wielokrotne nie jest dozwolone w Javie”, przyjmuję z przymrużeniem oka.

Dziedziczenie wielokrotne jest definiowane, gdy „Typ” dziedziczy z więcej niż jednego „Typu”. Interfejsy są również klasyfikowane jako typy, ponieważ mają zachowanie. Tak więc Java ma dziedziczenie wielokrotne. Tylko tyle, że jest bezpieczniejsze.

Rig Veda
źródło
6
Ale nie dziedziczysz po interfejsie, tylko go implementujesz. Interfejsy nie są klasami.
Blorgbeard wychodzi
2
Tak, ale dziedziczenie nigdy nie było tak wąsko zdefiniowane, z wyjątkiem niektórych książek wstępnych. Myślę, że powinniśmy wyjść poza gramatykę tego zagadnienia. Obaj robią to samo, a interfejsy zostały „wymyślone” tylko z tego powodu.
Rig Veda
Weź interfejs Closeable: jedna metoda, close (). Załóżmy, że chcesz rozszerzyć to na Openable: ma metodę open (), która ustawia pole boolowskie isOpen na true. Próba otwarcia otwartego elementu, który jest już otwarty, generuje wyjątek, podobnie jak próba zamknięcia otwartego elementu, który nie jest otwarty ... Setki ciekawszych genealogii klas mogą chcieć przyjąć taką funkcjonalność ... bez konieczności rozszerzania każdej z nich czas od zajęć Openable. Gdyby Openable był po prostu interfejsem, nie mógłby zapewnić takiej funkcjonalności. QED: interfejsy nie zapewniają dziedziczenia!
mike gryzoń
4

Dynamiczne ładowanie klas utrudnia implementację dziedziczenia wielokrotnego.

W Javie faktycznie uniknęli złożoności wielokrotnego dziedziczenia, zamiast tego wykorzystując pojedyncze dziedziczenie i interfejs. Złożoność wielokrotnego dziedziczenia jest bardzo duża w sytuacji opisanej poniżej

diamentowy problem wielokrotnego dziedziczenia. Mamy dwie klasy B i C dziedziczące po A. Załóżmy, że B i C przesłaniają odziedziczoną metodę i zapewniają własną implementację. Teraz D dziedziczy zarówno z B, jak i C, wykonując dziedziczenie wielokrotne. D powinien dziedziczyć tę zastąpioną metodę, jvm nie może zdecydować, która zastąpiona metoda zostanie użyta?

W języku c ++ funkcje wirtualne są używane do obsługi i musimy to zrobić jawnie.

Można tego uniknąć, używając interfejsów, nie ma treści metod. Nie można utworzyć instancji interfejsów - można je zaimplementować tylko przez klasy lub rozszerzyć o inne interfejsy.

sujith s
źródło
3

W rzeczywistości dziedziczenie wielokrotne będzie skomplikowane, jeśli odziedziczone klasy mają tę samą funkcję. tj. kompilator będzie miał zamieszanie, które trzeba wybrać (problem z diamentami). Tak więc w Javie ta złożoność usunęła się i dała interfejs, aby uzyskać funkcjonalność taką jak wielokrotne dziedziczenie. Możemy skorzystać z interfejsu

Jinu
źródło
2

Java ma koncept, czyli polimorfizm. W Javie istnieją 2 rodzaje polimorfizmu. Istnieje przeciążanie metod i zastępowanie metod. Wśród nich nadpisywanie metody odbywa się z relacją nad- i podklasową. Jeśli tworzymy obiekt podklasy i wywołujemy metodę nadklasy, a podklasa rozszerza więcej niż jedną klasę, jaką metodę superklasy należy wywołać?

Lub podczas wywoływania konstruktora nadklasy przez super(), który konstruktor superklasy zostanie wywołany?

Takie decyzje są niemożliwe ze względu na obecne funkcje Java API. więc dziedziczenie wielokrotne nie jest dozwolone w java.

Abhay Kokate
źródło
Zasadniczo system typów Javy zakłada, że ​​każda instancja obiektu ma jeden typ, rzutowanie obiektu na nadtyp zawsze będzie działało i zachowuje referencje, a rzutowanie odwołania do podtypu będzie zachowywało referencje, jeśli instancja jest tego podtypu lub jego podtyp. Takie założenia są przydatne i nie sądzę, aby można było dopuścić uogólnione wielokrotne dziedziczenie w sposób z nimi zgodny.
supercat
1

Dziedziczenie wielokrotne nie jest dozwolone bezpośrednio w Javie, ale jest dozwolone przez interfejsy.

Powód:

Dziedziczenie wielokrotne: wprowadza większą złożoność i niejednoznaczność.

Interfejsy: interfejsy są całkowicie abstrakcyjnymi klasami w Javie, które zapewniają jednolity sposób prawidłowego określenia struktury lub wewnętrznego działania programu z poziomu publicznie dostępnego interfejsu, czego konsekwencją jest większa elastyczność i kod wielokrotnego użytku, a także większa kontrola nad sposobem tworzenia innych klas i interakcji z nimi.

Mówiąc dokładniej, są one specjalną konstrukcją w Javie z dodatkową cechą, która umożliwia wykonywanie pewnego rodzaju dziedziczenia wielokrotnego, tj. Klas, które można upcastować do więcej niż jednej klasy.

Weźmy prosty przykład.

  1. Załóżmy, że istnieją 2 nadklasy klasy A i B z tymi samymi nazwami metod, ale różnymi funkcjami. Poprzez następujący kod ze słowem kluczowym (extends) nie jest możliwe wielokrotne dziedziczenie.

       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. Ale poprzez interfejsy, ze słowem kluczowym (implements) możliwe jest wielokrotne dziedziczenie.

    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
        }
    }
Chaitra Deshpande
źródło
0

Czy ktoś może mi dokładnie powiedzieć, dlaczego jest to zabronione?

Możesz znaleźć odpowiedź, korzystając z tego łącza do dokumentacji

Jednym z powodów, dla których język programowania Java nie pozwala na rozszerzenie więcej niż jednej klasy, jest uniknięcie problemów związanych z wielokrotnym dziedziczeniem stanu, czyli możliwością dziedziczenia pól z wielu klas

Jeśli wielokrotne dziedziczenie jest dozwolone i podczas tworzenia obiektu przez utworzenie instancji tej klasy, ten obiekt odziedziczy pola ze wszystkich superklas tej klasy. Spowoduje to dwa problemy.

  1. Co się stanie, jeśli metody lub konstruktory z różnych superklas utworzą instancję tego samego pola?

  2. Która metoda lub konstruktor będzie mieć pierwszeństwo?

Mimo że wielokrotne dziedziczenie stanu jest teraz dozwolone, nadal można zaimplementować

Wielokrotne dziedziczenie typu : zdolność klasy do implementacji więcej niż jednego interfejsu.

Wielokrotne dziedziczenie implementacji (poprzez domyślne metody w interfejsach): Możliwość dziedziczenia definicji metod z wielu klas

Dodatkowe informacje można znaleźć w tym powiązanym pytaniu SE:

Wielokrotna niejednoznaczność dziedziczenia z interfejsem

Ravindra babu
źródło
0

W C ++ klasa może dziedziczyć (bezpośrednio lub pośrednio) z więcej niż jednej klasy, co jest określane jako wielokrotne dziedziczenie .

Jednak języki C # i Java ograniczają klasy do pojedynczego dziedziczenia, które każda klasa dziedziczy z jednej klasy nadrzędnej.

Dziedziczenie wielokrotne to przydatny sposób tworzenia klas, które łączą aspekty dwóch różnych hierarchii klas, co często się zdarza, gdy używa się różnych struktur klas w jednej aplikacji.

Jeśli dwie struktury definiują własne klasy bazowe dla wyjątków, na przykład można użyć dziedziczenia wielokrotnego, aby utworzyć klasy wyjątków, które mogą być używane z dowolną strukturą.

Problem z dziedziczeniem wielokrotnym polega na tym, że może prowadzić do niejednoznaczności. Klasycznym przykładem jest sytuacja, gdy klasa dziedziczy po dwóch innych klasach, z których każda dziedziczy po tej samej klasie:

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
    }
};

W tym przykładzie element flagczłonkowski danych jest zdefiniowany przez class A. Ale class Dpochodzi od class B i class C, które wywodzą się z obu A, a więc w istocie dwie kopie z flagsą dostępne, ponieważ dwa przypadki Asą w D„s klasowej hierarchii. Który chcesz ustawić? Kompilator będzie narzekał, że odwołanie do flagin Djest niejednoznaczne . Jedną z poprawek jest jawne ujednoznacznienie odniesienia:

B::flag = nflag;

Innym rozwiązaniem jest zadeklarowanie B i C jako virtual base classes , co oznacza, że ​​tylko jedna kopia A może istnieć w hierarchii, eliminując wszelkie niejednoznaczności.

Istnieją inne zawiłości związane z dziedziczeniem wielokrotnym, na przykład kolejność inicjowania klas podstawowych podczas konstruowania obiektu pochodnego lub sposób, w jaki elementy członkowskie mogą być nieumyślnie ukrywane w klasach pochodnych. Aby uniknąć tych zawiłości, niektóre języki ograniczają się do prostszego pojedynczego modelu dziedziczenia.

Chociaż znacznie upraszcza to dziedziczenie, ogranicza również jego użyteczność, ponieważ tylko klasy mające wspólnego przodka mogą dzielić zachowania. Interfejsy nieco łagodzą to ograniczenie, umożliwiając klasom w różnych hierarchiach ujawnianie wspólnych interfejsów, nawet jeśli nie są one implementowane przez udostępnianie kodu.

zerocool
źródło
-1

Wyobraź sobie ten przykład: mam klasę Shape1

Ma CalcualteAreametodę:

Class Shape1
{

 public void CalculateArea()

     {
       //
     }
}

Jest inna klasa, Shape2która również ma tę samą metodę

Class Shape2
{

 public void CalculateArea()

     {

     }
}

Teraz mam podrzędną klasę Circle, która wywodzi się zarówno z Shape1, jak i Shape2;

public class Circle: Shape1, Shape2
{
}

Teraz, kiedy tworzę obiekt dla Circle i wywołuję tę metodę, system nie wie, którą metodę obliczania powierzchni należy wywołać. Obie mają takie same podpisy. Więc kompilator będzie się mylić. Dlatego wielokrotne dziedziczenie nie jest dozwolone.

Ale może być wiele interfejsów, ponieważ interfejsy nie mają definicji metody. Nawet oba interfejsy mają tę samą metodę, oba nie mają żadnej implementacji i zawsze zostanie wykonana metoda w klasie potomnej.

Engr Muhammad Enayet Abdullah
źródło
Projektant języka może podać jawne wywołanie metody, tak jak robi to w Interface. Nie ma takiego punktu! Innym powodem jest to, co powiesz na klasę abstrakcyjną z metodami abstrakcyjnymi, jak teraz to rozważasz?
Nomi Ali