Penanganan pengecualian (Pustaka Paralel Tugas)

Pengecualian yang tidak tertangani yang dilemparkan oleh kode pengguna yang berjalan di dalam tugas disebarluaskan kembali ke utas panggilan, kecuali dalam skenario tertentu yang dijelaskan nanti dalam topik ini. Pengecualian disebarluaskan saat Anda menggunakan salah satu metode statis atau instans Task.Wait, dan Anda menanganinya dengan menyertakan panggilan dalam pernyataan try/catch. Jika tugas adalah induk tugas turunan yang dilampirkan, atau jika Anda menunggu beberapa tugas, beberapa pengecualian dapat dilemparkan.

Untuk menyebarluaskan semua pengecualian kembali ke utas panggilan, infrastruktur Tugas membungkusnya dalam instans AggregateException. Pengecualian AggregateException memiliki properti InnerExceptions yang dapat dijumlahkan untuk memeriksa semua pengecualian asli yang dilemparkan, dan menangani (atau tidak menangani) masing-masing satu per satu. Anda juga dapat menangani pengecualian asli dengan menggunakan metode AggregateException.Handle.

Bahkan jika hanya satu pengecualian yang dilemparkan, itu masih dibungkus dalam pengecualian AggregateException, seperti yang ditunjukkan contoh berikut.


public static partial class Program
{
    public static void HandleThree()
    {
        var task = Task.Run(
            () => throw new CustomException("This exception is expected!"));

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            foreach (var ex in ae.InnerExceptions)
            {
                // Handle the custom exception.
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                }
                // Rethrow any other exception.
                else
                {
                    throw ex;
                }
            }
        }
    }
}
// The example displays the following output:
//        This exception is expected!
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Run(Sub() Throw New CustomException("This exception is expected!"))

        Try
            task1.Wait()
        Catch ae As AggregateException
            For Each ex In ae.InnerExceptions
                ' Handle the custom exception.
                If TypeOf ex Is CustomException Then
                    Console.WriteLine(ex.Message)
                    ' Rethrow any other exception.
                Else
                    Throw ex
                End If
            Next
        End Try
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays the following output:
'       This exception is expected!

Anda dapat menghindari pengecualian yang tidak tertangani hanya dengan menangkap AggregateException dan tidak mengamati pengecualian dalam apa pun. Namun, kami sarankan Anda tidak melakukan ini karena dianalogikan untuk menangkap jenis dasar Exception dalam skenario non-paralel. Untuk menangkap pengecualian tanpa mengambil tindakan tertentu untuk pulih darinya dapat membuat program Anda dalam keadaan yang tidak ditentukan.

Jika Anda tidak ingin memanggil metode Task.Wait untuk menunggu penyelesaian tugas, Anda juga dapat mengambil pengecualian AggregateException dari properti tugas Exception, seperti yang ditunjukkan contoh berikut. Untuk informasi selengkapnya, lihat bagian Mengamati pengecualian dengan menggunakan properti Task.Exception dalam topik ini.


public static partial class Program
{
    public static void HandleFour()
    {
        var task = Task.Run(
            () => throw new CustomException("This exception is expected!"));

        while (!task.IsCompleted) { }

        if (task.Status == TaskStatus.Faulted)
        {
            foreach (var ex in task.Exception?.InnerExceptions ?? new(Array.Empty<Exception>()))
            {
                // Handle the custom exception.
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                }
                // Rethrow any other exception.
                else
                {
                    throw ex;
                }
            }
        }
    }
}
// The example displays the following output:
//        This exception is expected!
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Run(Sub() Throw New CustomException("This exception is expected!"))

        While Not task1.IsCompleted
        End While

        If task1.Status = TaskStatus.Faulted Then
            For Each ex In task1.Exception.InnerExceptions
                ' Handle the custom exception.
                If TypeOf ex Is CustomException Then
                    Console.WriteLine(ex.Message)
                    ' Rethrow any other exception.
                Else
                    Throw ex
                End If
            Next
        End If
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays the following output:
'       This exception is expected!

Perhatian

Contoh kode sebelumnya mencakup perulangan while yang melakukan polling properti Task.IsCompleted tugas untuk menentukan kapan tugas telah selesai. Hal ini tidak boleh dilakukan dalam kode produksi karena sangat tidak efisien.

Jika Anda tidak menunggu tugas yang menyebarkan pengecualian, atau mengakses properti Exception, pengecualian dieskalasikan sesuai dengan kebijakan pengecualian .NET saat tugas dikumpulkan sampah.

Saat pengecualian diizinkan untuk menggelembung kembali ke utas gabungan, ada kemungkinan bahwa tugas dapat terus memproses beberapa item setelah pengecualian dinaikkan.

Catatan

Saat "Hanya Kode Saya" diaktifkan, Visual Studio dalam beberapa kasus akan terputus pada baris yang melemparkan pengecualian dan menampilkan pesan kesalahan yang mengatakan "pengecualian tidak ditangani oleh kode pengguna." Kesalahan ini tidak berbahaya. Anda dapat menekan F5 untuk melanjutkannya, dan melihat perilaku penanganan pengecualian yang ditunjukkan dalam contoh di bawah ini. Untuk mencegah Visual Studio melanggar kesalahan pertama, cukup hapus centang pada kotak centang Aktifkan Hanya Kode Saya di bawah Alat, Opsi, Penelusuran Kesalahan, Umum.

Tugas turunan terlampir dan AggregateExceptions berlapis

Jika tugas memiliki tugas turunan terlampir yang melempar pengecualian, pengecualian tersebut dibungkus dalam AggregateException sebelum disebarluaskan ke tugas induk, yang membungkus pengecualian itu di dalam AggregateException sendiri sebelum menyebarkannya kembali ke utas panggilan. Dalam kasus seperti itu, properti InnerExceptions dari pengecualian AggregateException yang tertangkap pada metode Task.Wait, WaitAny, atau WaitAll berisi satu atau beberapa instans AggregateException, bukan pengecualian asli yang menyebabkan kesalahan. Untuk menghindari keharusan mengulangi pengecualian AggregateException berlapis, Anda dapat menggunakan metode Flatten untuk menghapus semua pengecualian AggregateException berlapis, sehingga properti AggregateException.InnerExceptions berisi pengecualian asli. Dalam contoh berikut, instans AggregateException berlapis diratakan dan ditangani hanya dalam satu perulangan.


public static partial class Program
{
    public static void FlattenTwo()
    {
        var task = Task.Factory.StartNew(() =>
        {
            var child = Task.Factory.StartNew(() =>
            {
                var grandChild = Task.Factory.StartNew(() =>
                {
                    // This exception is nested inside three AggregateExceptions.
                    throw new CustomException("Attached child2 faulted.");
                }, TaskCreationOptions.AttachedToParent);

                // This exception is nested inside two AggregateExceptions.
                throw new CustomException("Attached child1 faulted.");
            }, TaskCreationOptions.AttachedToParent);
        });

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            foreach (var ex in ae.Flatten().InnerExceptions)
            {
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                }
                else
                {
                    throw;
                }
            }
        }
    }
}
// The example displays the following output:
//    Attached child1 faulted.
//    Attached child2 faulted.
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Factory.StartNew(Sub()
                                              Dim child1 = Task.Factory.StartNew(Sub()
                                                                                     Dim child2 = Task.Factory.StartNew(Sub()
                                                                                                                            Throw New CustomException("Attached child2 faulted.")
                                                                                                                        End Sub,
                                                                                                                        TaskCreationOptions.AttachedToParent)
                                                                                     Throw New CustomException("Attached child1 faulted.")
                                                                                 End Sub,
                                                                                 TaskCreationOptions.AttachedToParent)
                                          End Sub)

        Try
            task1.Wait()
        Catch ae As AggregateException
            For Each ex In ae.Flatten().InnerExceptions
                If TypeOf ex Is CustomException Then
                    Console.WriteLine(ex.Message)
                Else
                    Throw
                End If
            Next
        End Try
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays the following output:
'       Attached child1 faulted.
'       Attached child2 faulted.

Anda juga dapat menggunakan metode AggregateException.Flatten untuk menumbuhkan kembali pengecualian dalam dari beberapa instans AggregateException yang dilemparkan oleh beberapa tugas dalam instans AggregateException tunggal, seperti yang ditunjukkan contoh berikut.

public static partial class Program
{
    public static void TaskExceptionTwo()
    {
        try
        {
            ExecuteTasks();
        }
        catch (AggregateException ae)
        {
            foreach (var e in ae.InnerExceptions)
            {
                Console.WriteLine(
                    "{0}:\n   {1}", e.GetType().Name, e.Message);
            }
        }
    }

    static void ExecuteTasks()
    {
        // Assume this is a user-entered String.
        string path = @"C:\";
        List<Task> tasks = new();

        tasks.Add(Task.Run(() =>
        {
            // This should throw an UnauthorizedAccessException.
            return Directory.GetFiles(
                path, "*.txt",
                SearchOption.AllDirectories);
        }));

        tasks.Add(Task.Run(() =>
        {
            if (path == @"C:\")
            {
                throw new ArgumentException(
                    "The system root is not a valid path.");
            }
            return new string[] { ".txt", ".dll", ".exe", ".bin", ".dat" };
        }));

        tasks.Add(Task.Run(() =>
        {
            throw new NotImplementedException(
                "This operation has not been implemented.");
        }));

        try
        {
            Task.WaitAll(tasks.ToArray());
        }
        catch (AggregateException ae)
        {
            throw ae.Flatten();
        }
    }
}
// The example displays the following output:
//       UnauthorizedAccessException:
//          Access to the path 'C:\Documents and Settings' is denied.
//       ArgumentException:
//          The system root is not a valid path.
//       NotImplementedException:
//          This operation has not been implemented.
Imports System.Collections.Generic
Imports System.IO
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Try
            ExecuteTasks()
        Catch ae As AggregateException
            For Each e In ae.InnerExceptions
                Console.WriteLine("{0}:{2}   {1}", e.GetType().Name, e.Message,
                                  vbCrLf)
            Next
        End Try
    End Sub

    Sub ExecuteTasks()
        ' Assume this is a user-entered String.
        Dim path = "C:\"
        Dim tasks As New List(Of Task)

        tasks.Add(Task.Run(Function()
                               ' This should throw an UnauthorizedAccessException.
                               Return Directory.GetFiles(path, "*.txt",
                                                         SearchOption.AllDirectories)
                           End Function))

        tasks.Add(Task.Run(Function()
                               If path = "C:\" Then
                                   Throw New ArgumentException("The system root is not a valid path.")
                               End If
                               Return {".txt", ".dll", ".exe", ".bin", ".dat"}
                           End Function))

        tasks.Add(Task.Run(Sub()
                               Throw New NotImplementedException("This operation has not been implemented.")
                           End Sub))

        Try
            Task.WaitAll(tasks.ToArray)
        Catch ae As AggregateException
            Throw ae.Flatten()
        End Try
    End Sub
End Module
' The example displays the following output:
'       UnauthorizedAccessException:
'          Access to the path 'C:\Documents and Settings' is denied.
'       ArgumentException:
'          The system root is not a valid path.
'       NotImplementedException:
'          This operation has not been implemented.

Pengecualian dari tugas turunan yang dilepas

Secara default, tugas turunan dibuat sebagai dilepaskan. Pengecualian yang dilemparkan dari tugas yang dilepas harus ditangani atau ditumbuhkan kembali dalam tugas induk langsung; mereka tidak disebarluaskan kembali ke utas panggilan dengan cara yang sama seperti tugas turunan terlampir yang disebarluaskan kembali. Induk paling atas dapat secara manual menumbuhkan kembali pengecualian dari turunan yang dilepaskan untuk menyebabkannya dibungkus dalam AggregateException dan disebarkan kembali ke utas panggilan.


public static partial class Program
{
    public static void DetachedTwo()
    {
        var task = Task.Run(() =>
        {
            var nestedTask = Task.Run(
                () => throw new CustomException("Detached child task faulted."));

            // Here the exception will be escalated back to the calling thread.
            // We could use try/catch here to prevent that.
            nestedTask.Wait();
        });

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            foreach (var e in ae.Flatten().InnerExceptions)
            {
                if (e is CustomException)
                {
                    Console.WriteLine(e.Message);
                }
            }
        }
    }
}
// The example displays the following output:
//    Detached child task faulted.
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Run(Sub()
                                 Dim nestedTask1 = Task.Run(Sub()
                                                                Throw New CustomException("Detached child task faulted.")
                                                            End Sub)
                                 ' Here the exception will be escalated back to joining thread.
                                 ' We could use try/catch here to prevent that.
                                 nestedTask1.Wait()
                             End Sub)

        Try
            task1.Wait()
        Catch ae As AggregateException
            For Each ex In ae.Flatten().InnerExceptions
                If TypeOf ex Is CustomException Then
                    ' Recover from the exception. Here we just
                    ' print the message for demonstration purposes.
                    Console.WriteLine(ex.Message)
                End If
            Next
        End Try
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays the following output:
'       Detached child task faulted.

Bahkan jika Anda menggunakan kelanjutan untuk mengamati pengecualian dalam tugas turunan, pengecualian masih harus diamati oleh tugas induk.

Pengecualian yang menunjukkan pembatalan kooperatif

Saat kode pengguna dalam tugas merespons permintaan pembatalan, prosedur yang benar adalah melemparkan passing OperationCanceledException dalam token pembatalan tempat permintaan dikomunikasikan. Sebelum mencoba menyebarluaskan pengecualian, instans tugas membandingkan token dalam pengecualian dengan yang diteruskan ke dalamnya saat dibuat. Jika sama, tugas menyebarkan TaskCanceledException yang dibungkus dalam AggregateException, dan dapat dilihat saat pengecualian dalam diperiksa. Namun, jika utas panggilan tidak menunggu tugas, pengecualian khusus ini tidak akan disebarluaskan. Untuk informasi selengkapnya, lihat Pembatalan Tugas.

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var task = Task.Factory.StartNew(() =>
{
    CancellationToken ct = token;
    while (someCondition)
    {
        // Do some work...
        Thread.SpinWait(50_000);
        ct.ThrowIfCancellationRequested();
    }
},
token);

// No waiting required.
tokenSource.Dispose();
Dim someCondition As Boolean = True
Dim tokenSource = New CancellationTokenSource()
Dim token = tokenSource.Token

Dim task1 = Task.Factory.StartNew(Sub()
                                      Dim ct As CancellationToken = token
                                      While someCondition = True
                                          ' Do some work...
                                          Thread.SpinWait(500000)
                                          ct.ThrowIfCancellationRequested()
                                      End While
                                  End Sub,
                                  token)

Menggunakan metode handle untuk memfilter pengecualian dalam

Anda dapat menggunakan metode AggregateException.Handle untuk memfilter pengecualian yang dapat Anda perlakukan sebagai "ditangani" tanpa menggunakan logika lebih lanjut. Dalam delegasi pengguna yang diberikan ke metode AggregateException.Handle(Func<Exception,Boolean>), Anda dapat memeriksa jenis pengecualian, properti Message, atau informasi lain tentangnya yang akan memungkinkan Anda menentukan apakah itu jinak. Setiap pengecualian yang delegasinya mengembalikan false dilempar kembali dalam instans baru AggregateException segera setelah metode AggregateException.Handle kembali.

Contoh berikut secara fungsional setara dengan contoh pertama dalam topik ini, yang memeriksa setiap pengecualian dalam koleksi AggregateException.InnerExceptions. Sebaliknya, handler pengecualian ini memanggil objek metode AggregateException.Handle untuk setiap pengecualian, dan hanya menumbuhkan kembali pengecualian yang bukan instans CustomException.


public static partial class Program
{
    public static void HandleMethodThree()
    {
        var task = Task.Run(
            () => throw new CustomException("This exception is expected!"));

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            // Call the Handle method to handle the custom exception,
            // otherwise rethrow the exception.
            ae.Handle(ex =>
            {
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                }
                return ex is CustomException;
            });
        }
    }
}
// The example displays the following output:
//        This exception is expected!
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Run(Sub() Throw New CustomException("This exception is expected!"))

        Try
            task1.Wait()
        Catch ae As AggregateException
            ' Call the Handle method to handle the custom exception,
            ' otherwise rethrow the exception.
            ae.Handle(Function(e)
                          If TypeOf e Is CustomException Then
                              Console.WriteLine(e.Message)
                          End If
                          Return TypeOf e Is CustomException
                      End Function)
        End Try
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays the following output:
'       This exception is expected!

Berikut ini adalah contoh yang lebih lengkap yang menggunakan AggregateException.Handle metode untuk memberikan penanganan khusus untuk pengecualian UnauthorizedAccessException saat menghitung file.

public static partial class Program
{
    public static void TaskException()
    {
        // This should throw an UnauthorizedAccessException.
        try
        {
            if (GetAllFiles(@"C:\") is { Length: > 0 } files)
            {
                foreach (var file in files)
                {
                    Console.WriteLine(file);
                }
            }
        }
        catch (AggregateException ae)
        {
            foreach (var ex in ae.InnerExceptions)
            {
                Console.WriteLine(
                    "{0}: {1}", ex.GetType().Name, ex.Message);
            }
        }
        Console.WriteLine();

        // This should throw an ArgumentException.
        try
        {
            foreach (var s in GetAllFiles(""))
            {
                Console.WriteLine(s);
            }
        }
        catch (AggregateException ae)
        {
            foreach (var ex in ae.InnerExceptions)
                Console.WriteLine(
                    "{0}: {1}", ex.GetType().Name, ex.Message);
        }
    }

    static string[] GetAllFiles(string path)
    {
        var task1 =
            Task.Run(() => Directory.GetFiles(
                path, "*.txt",
                SearchOption.AllDirectories));

        try
        {
            return task1.Result;
        }
        catch (AggregateException ae)
        {
            ae.Handle(x =>
            {
                // Handle an UnauthorizedAccessException
                if (x is UnauthorizedAccessException)
                {
                    Console.WriteLine(
                        "You do not have permission to access all folders in this path.");
                    Console.WriteLine(
                        "See your network administrator or try another path.");
                }
                return x is UnauthorizedAccessException;
            });
            return Array.Empty<string>();
        }
    }
}
// The example displays the following output:
//       You do not have permission to access all folders in this path.
//       See your network administrator or try another path.
//
//       ArgumentException: The path is not of a legal form.
Imports System.IO
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        ' This should throw an UnauthorizedAccessException.
        Try
            Dim files = GetAllFiles("C:\")
            If files IsNot Nothing Then
                For Each file In files
                    Console.WriteLine(file)
                Next
            End If
        Catch ae As AggregateException
            For Each ex In ae.InnerExceptions
                Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message)
            Next
        End Try
        Console.WriteLine()

        ' This should throw an ArgumentException.
        Try
            For Each s In GetAllFiles("")
                Console.WriteLine(s)
            Next
        Catch ae As AggregateException
            For Each ex In ae.InnerExceptions
                Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message)
            Next
        End Try
        Console.WriteLine()
    End Sub

    Function GetAllFiles(ByVal path As String) As String()
        Dim task1 = Task.Run(Function()
                                 Return Directory.GetFiles(path, "*.txt",
                                                           SearchOption.AllDirectories)
                             End Function)
        Try
            Return task1.Result
        Catch ae As AggregateException
            ae.Handle(Function(x)
                          ' Handle an UnauthorizedAccessException
                          If TypeOf x Is UnauthorizedAccessException Then
                              Console.WriteLine("You do not have permission to access all folders in this path.")
                              Console.WriteLine("See your network administrator or try another path.")
                          End If
                          Return TypeOf x Is UnauthorizedAccessException
                      End Function)
        End Try
        Return Array.Empty(Of String)()
    End Function
End Module
' The example displays the following output:
'       You do not have permission to access all folders in this path.
'       See your network administrator or try another path.
'
'       ArgumentException: The path is not of a legal form.

Mengamati pengecualian menggunakan properti Task.Exception

Jika tugas selesai dalam status TaskStatus.Faulted, properti Exception dapat diperiksa untuk menemukan pengecualian spesifik mana yang menyebabkan kesalahan. Cara yang baik untuk mengamati properti Exception adalah dengan menggunakan kelanjutan yang hanya berjalan jika kesalahan tugas antecedent, seperti yang ditunjukkan dalam contoh berikut.


public static partial class Program
{
    public static void ExceptionPropagationTwo()
    {
        _ = Task.Run(
            () => throw new CustomException("task1 faulted."))
            .ContinueWith(_ =>
            {
                if (_.Exception?.InnerException is { } inner)
                {
                    Console.WriteLine("{0}: {1}",
                        inner.GetType().Name,
                        inner.Message);
                }
            }, 
            TaskContinuationOptions.OnlyOnFaulted);
        
        Thread.Sleep(500);
    }
}
// The example displays output like the following:
//        CustomException: task1 faulted.
Imports System.Threading
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Factory.StartNew(Sub()
                                              Throw New CustomException("task1 faulted.")
                                          End Sub).
                    ContinueWith(Sub(t)
                                     Console.WriteLine("{0}: {1}",
                                                     t.Exception.InnerException.GetType().Name,
                                                     t.Exception.InnerException.Message)
                                 End Sub, TaskContinuationOptions.OnlyOnFaulted)

        Thread.Sleep(500)
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays output like the following:
'       CustomException: task1 faulted.

Dalam aplikasi yang bermakna, delegasi kelanjutan dapat mencatat informasi terperinci tentang pengecualian dan mungkin menghasilkan tugas baru untuk pulih dari pengecualian. Jika kesalahan tugas, ekspresi berikut melemparkan pengecualian:

  • await task
  • task.Wait()
  • task.Result
  • task.GetAwaiter().GetResult()

Gunakan pernyataan try-catch untuk menangani dan mengamati pengecualian yang dilemparkan. Atau, amati pengecualian dengan mengakses properti Task.Exception.

Penting

AggregateException tidak dapat ditangkap secara eksplisit saat menggunakan ekspresi berikut:

  • await task
  • task.GetAwaiter().GetResult()

Peristiwa UnobservedTaskException

Dalam beberapa skenario, seperti saat menghosting plug-in yang tidak tepercaya, pengecualian jinak mungkin umum, dan mungkin terlalu sulit untuk mengamati semuanya secara manual. Dalam kasus ini, Anda dapat menangani peristiwa TaskScheduler.UnobservedTaskException. Instans System.Threading.Tasks.UnobservedTaskExceptionEventArgs yang diteruskan ke handler Anda dapat digunakan untuk mencegah pengecualian yang tidak ditangguhkan disebarluaskan kembali ke utas gabungan.

Lihat juga