Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Jenis union mewakili nilai yang bisa menjadi salah satu dari beberapa jenis kasus. Serikat pekerja menyediakan konversi implisit dari setiap jenis kasus, pencocokan pola lengkap, dan pelacakan nullability yang ditingkatkan.
union Gunakan kata kunci untuk mendeklarasikan jenis serikat:
public union Pet(Cat, Dog, Bird);
Deklarasi ini membuat serikat Pet dengan tiga jenis kasus: Cat, Dog, dan Bird. Anda dapat menetapkan nilai jenis kasus apa pun ke Pet variabel. Pengkompilasi memastikan bahwa switch ekspresi mencakup semua jenis kasus.
Referensi bahasa C# mendokumentasikan versi bahasa C# yang paling baru dirilis. Ini juga berisi dokumentasi awal untuk fitur dalam pratinjau publik untuk rilis bahasa yang akan datang.
Dokumentasi mengidentifikasi fitur apa pun yang pertama kali diperkenalkan dalam tiga versi terakhir bahasa atau dalam pratinjau publik saat ini.
Petunjuk / Saran
Untuk menemukan kapan fitur pertama kali diperkenalkan di C#, lihat artikel tentang riwayat versi bahasa C#.
Nyatakan serikat ketika nilai harus tepat salah satu dari sekumpulan jenis tetap dan Anda ingin pengkompilasi memberlakukan bahwa setiap kemungkinan ditangani. Skenario umum meliputi:
-
Hasil kesalahan: Metode mengembalikan nilai keberhasilan atau nilai kesalahan, dan pemanggil harus menangani keduanya. Serikat seperti
union Result(Success, Error)membuat serangkaian hasil menjadi eksplisit. -
Pengiriman pesan atau perintah: Sistem memproses sekumpulan jenis pesan tertutup. Gabungan memastikan jenis pesan baru menghasilkan peringatan waktu kompilasi di setiap
switchyang belum menanganinya. - Mengganti antarmuka penanda atau kelas dasar abstrak: Jika Anda menggunakan antarmuka atau kelas abstrak semata-mata untuk jenis grup untuk pencocokan pola, serikat pekerja memberi Anda pemeriksaan kelelahan tanpa memerlukan warisan atau anggota bersama.
Serikat pekerja berbeda dari deklarasi jenis lain dengan cara penting:
-
classTidak seperti ataustruct, serikat tidak menentukan anggota data baru. Sebaliknya, ia menyusun jenis yang ada ke dalam sekumpulan alternatif tertutup. -
interfaceTidak seperti , serikat tertutup—Anda menentukan daftar lengkap jenis kasus dalam deklarasi, dan pengkompilasi menggunakan daftar tersebut untuk pemeriksaan kelelahan. -
recordTidak seperti , serikat tidak menambahkan perilaku kesetaraan, kloning, atau dekonstruksi. Serikat berfokus pada "kasus mana itu?" daripada "bidang apa yang dimilikinya?"
Jenis union mengandalkan UnionAttribute jenis dan IUnion di System.Runtime.CompilerServices namespace. Runtime .NET 11 mencakup jenis ini.
Deklarasi serikat
Deklarasi serikat kerja menentukan nama dan daftar jenis kasus:
public union Pet(Cat, Dog, Bird);
Jenis kasus dapat berupa jenis apa pun yang dikonversi ke object, termasuk kelas, struct, antarmuka, parameter jenis, jenis nullable, dan serikat lainnya. Contoh berikut menunjukkan kemungkinan jenis kasus yang berbeda:
public record class Cat(string Name);
public record class Dog(string Name, string Breed);
public record class Bird(string Name);
public record class None;
public record class Some<T>(T Value);
public union Option<T>(None, Some<T>);
public union IntOrString(int, string);
Ketika jenis kasus adalah jenis nilai (seperti int), deklarasi serikat kotak nilai dan menyimpannya sebagai referensi tunggal object? .
Jenis serikat kustom dapat menggunakan strategi penyimpanan yang berbeda.
Deklarasi serikat dapat mencakup isi dengan anggota tambahan, sama seperti struktur, tunduk pada beberapa batasan. Deklarasi serikat tidak dapat menyertakan bidang instans, properti otomatis, atau peristiwa seperti bidang. Anda juga tidak dapat mendeklarasikan konstruktor publik dengan satu parameter, karena kompilator menghasilkan konstruktor tersebut sebagai anggota pembuatan serikat pekerja. Setiap konstruktor yang Anda nyatakan harus menggunakan this(...) penginisialisasi untuk mendelegasikan, secara langsung atau tidak langsung, ke konstruktor yang dihasilkan. Gabungan berikut Length menambahkan TotalMeters properti yang menggunakan pencocokan pola untuk menangani setiap jenis kasus, bersama dengan Add metode yang menggabungkan dua panjang:
public record class Meters(double Value);
public record class Feet(double Value);
public union Length(Meters, Feet)
{
public double TotalMeters => this switch
{
Meters m => m.Value,
Feet f => f.Value * 0.3048,
_ => throw new InvalidOperationException("The Length has no value."),
};
public Length Add(Length other) => new Meters(TotalMeters + other.TotalMeters);
}
Konversi serikat pekerja
Konversi serikat implisit ada dari setiap jenis kasus ke jenis serikat:
static void BasicConversion()
{
Pet pet = new Dog("Rex", "Labrador");
Console.WriteLine(pet.Value); // output: Dog { Name = Rex, Breed = Labrador }
Pet pet2 = new Cat("Whiskers");
Console.WriteLine(pet2.Value); // output: Cat { Name = Whiskers }
}
Konversi gabungan berfungsi dengan memanggil konstruktor yang dihasilkan yang sesuai. Jika operator konversi implisit yang ditentukan pengguna ada untuk jenis yang sama, operator yang ditentukan pengguna lebih diprioritaskan daripada konversi serikat pekerja. Jika lebih dari satu jenis kasus sama-sama berlaku untuk nilai sumber, konversi gabungan ambigu, dan pengompilasi melaporkan kesalahan. Untuk detail tentang prioritas konversi, lihat spesifikasi fitur.
Konversi union ke union struct nullable (T?) juga berfungsi ketika T adalah jenis union:
static void NullableUnionExample()
{
Pet? maybePet = new Dog("Buddy", "Beagle");
Pet? noPet = null;
Console.WriteLine(Describe(maybePet)); // output: Buddy
Console.WriteLine(Describe(noPet)); // output: no pet
static string Describe(Pet? pet) => pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
null => "no pet",
};
}
Pencocokan pola gabungan
Saat Anda menggunakan pola jenis atau deklarasi pada ekspresi gabungan, pola menguji konten serikat, bukan instans gabungan. Misalnya, Dog pola menguji apakah Value adalah Dog:
static void PatternMatching()
{
Pet pet = new Dog("Rex", "Labrador");
var name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
Console.WriteLine(name); // output: Rex
}
Formulir pola menentukan nilai mana yang diujinya:
| Pattern | Perilaku pada ekspresi serikat |
|---|---|
Jenis atau pola deklarasi, seperti Dog atau Dog dog |
Menguji konten terhadap jenis yang ditentukan. |
Pola properti atau posisi dengan jenis, seperti Dog { Breed: "Labrador" } |
Menguji konten terhadap jenis, lalu menerapkan pola berlapis ke nilai kasus tersebut. |
Pola properti atau posisi tanpa jenis, seperti { Name: "Rex" } |
Menggunakan properti atau Deconstruct metode instans union. |
| Pola daftar | Menggunakan jumlah instans gabungan dan anggota pengindeksan, bukan kontennya. |
var atau buang _ pola |
Menangkap atau mencocokkan instans union masuk tanpa membongkarnya. |
| Pola konstanta atau relasional | Menguji konten. Pola ini null juga menangani referensi gabungan null atau pembungkus null. |
Misalnya, tentukan gabungan Pet dengan properti baca-saja Name yang mendelegasikan ke yang terkandung Cat atau Dog. Pola jenis dalam ekspresi properti switch menguji konten:
public union Pet(Cat, Dog)
{
public string Name => this switch
{
Cat cat => cat.Name,
Dog dog => dog.Name,
null => "",
};
}
Pola jenis Dog { Breed: "Labrador" } menguji konten dan membaca anjing Breed, properti yang tidak dimiliki serikat. Pola yang tidak dititik { Name: "Rex" } membaca properti serikat sebagai Name gantinya, sehingga juga cocok dengan kucing bernama Rex:
Pet pet = new Dog("Rex", "Labrador");
Console.WriteLine(pet is Dog { Breed: "Labrador" }); // output: True
Console.WriteLine(pet is { Name: "Rex" }); // output: True
pet = new Cat("Rex");
Console.WriteLine(pet is Dog { Breed: "Labrador" }); // output: False
Console.WriteLine(pet is { Name: "Rex" }); // output: True
Pola meniadakan not pola berlapisnya. Dalam pet is not null, pola berlapis null memeriksa konten, jadi not null adalah pengujian has-a-value yang berguna. Sebaliknya, pola properti kosong yang tidak dititik, { }, memeriksa instans union. Ini selalu cocok dengan union struct yang tidak dapat diurungkan, bahkan ketika Value null:
Pet empty = default;
Console.WriteLine(empty is { }); // output: True
Console.WriteLine(empty is not null); // output: False
if (empty is var captured)
{
Console.WriteLine(captured.Value is null); // output: True
}
Pola logis mempertahankan perbedaan ini. Dalam var pet and not null, var pet menangkap serikat masuk, dan not null menguji apakah memiliki konten non-null. Dalam not null and var pet, var pola masih menangkap penyatuan masuk: not tidak mengubah nilai yang diteruskan ke pola sebelah kanan. Sebaliknya, dalam Dog and { Breed: "Labrador" }, pola jenis meneruskan Dog yang cocok dengan pola properti.
Nota
Pola jenis harus kompatibel dengan setidaknya satu jenis kasus.
Pet Untuk deklarasi dalam artikel ini, pet is Pet menghasilkan kesalahan pengkompilasi CS8121: tidak ada jenis kasus yang dapat cocok Petdengan . Ini bukan pengujian runtime instans union. Untuk memeriksa konten, gunakan pet is not null atau pet.Value is not null; untuk menangkap serikat itu sendiri, gunakan var pola.
Singkatnya, pola jenis dan deklarasi menguji konten. Pola rekursif dengan jenis menguji konten juga; pola rekursif tanpa jenis menggunakan anggota serikat sendiri. Aturan ini sama dalam is ekspresi dan switch ekspresi.
Pencocokan null
Untuk penyatuan struct, null pola memeriksa apakah Value null:
static void NullHandling()
{
Pet pet = default;
var description = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
null => "no pet",
};
Console.WriteLine(description); // output: no pet
Console.WriteLine(pet.Value is null); // output: True
}
Untuk serikat berbasis kelas, null berhasil ketika referensi serikat itu sendiri null atau propertinya Value null:
Result<string>? result = null;
if (result is null) { /* true — the reference is null */ }
Result<string> empty = new Result<string>((string?)null);
if (empty is null) { /* true — Value is null */ }
Untuk jenis struct union nullable (Pet?), null berhasil ketika pembungkus nullable tidak memiliki nilai atau ketika union Value yang mendasar adalah null.
Kelelahan serikat
Ekspresi switch lengkap atas jenis kasus serikat saat menangani setiap jenis kasus. Jika analisis nullability juga mempertimbangkan konten non-null, Anda tidak memerlukan buang (_) atau var arm:
static void PatternMatching()
{
Pet pet = new Dog("Rex", "Labrador");
var name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
Console.WriteLine(name); // output: Rex
}
Untuk menangani konten null, pilih null lengan di atas var lengan atau buang (_). Pola menyatakan null niat Anda dan mempertahankan peringatan saat Anda menambahkan jenis kasus baru tanpa lengan yang cocok. Lengan var atau buang juga menerima kasus baru, sehingga pengkompilasi tidak dapat memperingatkan Anda tentang kasus yang hilang.
Ketika status Value null yang dilacak adalah "mungkin null," pengkompilasi juga memperingatkan jika Anda menghilangkan null lengan. Misalnya, NullablePet memiliki jenis huruf besar/kecil yang dapat diubah ke null, sehingga konten parameternya mungkin null:
public union NullablePet(Cat?, Dog, Bird);
static string DescribeNullableCase(NullablePet pet) => pet switch
{
Cat cat => cat.Name,
Dog dog => dog.Name,
Bird bird => bird.Name,
null => "no pet",
};
Menghapus null lengan dari sakelar ini menghasilkan peringatan CS8655.
Analisis nullable tidak menjamin bahwa union yang dihasilkan memiliki konten non-null saat runtime. Gabungan default berisi null, termasuk saat merupakan bidang atau elemen array yang diinisialisasi default. Jika semua jenis kasus tidak dapat diulang, status Value terlacak default tetap "tidak null," sehingga pengkompilasi tidak selalu memperingatkan tentang lengan yang dihilangkan null . Tangani null untuk kebenaran setiap kali konten default dapat mencapai sakelar Anda:
static void NullHandling()
{
Pet pet = default;
var description = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
null => "no pet",
};
Console.WriteLine(description); // output: no pet
Console.WriteLine(pet.Value is null); // output: True
}
Untuk contoh ini Pet , menghapus null lengan tidak menghasilkan peringatan missing-null-arm kecuali kode sebelumnya mengubah status null yang dilacak, tetapi sakelar melempar SwitchExpressionException saat menerima nilai default. Pengujian sebelumnya is null dapat mengubah status terlacak dan menyebabkan peringatan. Perilaku ini terpisah dari penugasan pasti: pengkompilasi menolak pembacaan variabel lokal yang tidak ditetapkan.
Keterbatalan
Pengkompilasi melacak status null properti gabungan Value melalui aturan berikut:
- Status null default properti serikat adalah
Value"mungkin null" jika status null default dari jenis kasus apa pun adalah "mungkin null." Jika tidak, status null default adalah "bukan null." - Saat Anda membuat nilai union dari jenis kasus (melalui konstruktor atau konversi union),
Valuemendapatkan status null dari nilai masuk. - Saat pola
HasValueakses non-tinju atauTryGetValue(...)anggota mengkueri konten serikat, statusValuenull menjadi "tidak null" padatruecabang.
Jenis serikat kustom
Kompilator mengonversi union deklarasi menjadi struct deklarasi. Struktur ditandai dengan [System.Runtime.CompilerServices.Union] atribut dan mengimplementasikan IUnion antarmuka. Ini termasuk konstruktor publik untuk setiap jenis kasus dan Value properti. Bahasa ini menyediakan konversi serikat implisit yang memanggil konstruktor tersebut. Bentuk yang dihasilkan dipendapatkan. Ini selalu merupakan struktur, selalu kotak kasus jenis nilai, dan selalu menyimpan konten sebagai object?.
Anda mungkin memerlukan perilaku yang berbeda jika Anda ingin menyesuaikan jenis yang ada, membuat serikat berbasis kelas, atau menggunakan strategi penyimpanan kustom, atau jika Anda memerlukan dukungan interop. Anda dapat membuat jenis serikat secara manual.
Kelas atau struktur apa pun dengan [Union] atribut adalah jenis gabungan jika mengikuti pola penyatuan dasar. Pola penyatuan dasar memerlukan:
- Atribut
[Union]pada jenis . - Satu atau beberapa konstruktor publik, masing-masing dengan satu menurut nilai atau
inparameter. Jenis parameter setiap konstruktor mendefinisikan jenis kasus. - Properti publik
Valueberjenisobject?(atauobject) dengangetaksesor.
Semua anggota serikat sebelumnya harus publik. Kompilator menggunakan anggota ini untuk menerapkan konversi serikat pekerja, pencocokan pola, dan pemeriksaan kelelahan. Anda juga dapat menerapkan pola akses non-tinju atau membuat jenis serikat berbasis kelas. Jenis serikat kustom Anda dapat menambahkan lebih banyak anggota.
Pengkompilasi mengasumsikan bahwa jenis serikat kustom memenuhi aturan perilaku ini:
-
Kedengaran:
Valueselalu mengembalikannullatau nilai dari salah satu jenis kasus, termasuk untuk nilai struct default. Gabungan kustom dapat mewakili nilai defaultnya sebagai kasus non-null, tidak seperti konten null dari gabungan yang dihasilkan default. -
Stabilitas: Jika Anda membuat nilai serikat dari jenis kasus,
Valuecocok dengan jenis kasus tersebut (atau apakahnullinputnya adalahnull). - Kesetaraan pembuatan: Jika nilai secara implisit dapat dikonversi ke dua jenis kasus yang berbeda, kedua anggota pembuatan menghasilkan perilaku yang dapat diamati yang sama.
-
Konsistensi pola akses: Anggota
HasValuedanTryGetValue, jika ada, berperilaku setara dengan memeriksaValuesecara langsung.
Contoh berikut menunjukkan jenis serikat kustom:
[System.Runtime.CompilerServices.Union]
public struct Shape : System.Runtime.CompilerServices.IUnion
{
private readonly object? _value;
public Shape(Circle value) { _value = value; }
public Shape(Rectangle value) { _value = value; }
public object? Value => _value;
}
public record class Circle(double Radius);
public record class Rectangle(double Width, double Height);
static void ManualUnionExample()
{
Shape shape = new Shape(new Circle(5.0));
var area = shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height,
};
Console.WriteLine($"{area:F2}"); // output: 78.54
}
Pola akses non-tinju
Jenis gabungan kustom dapat secara opsional menerapkan pola akses non-tinju untuk memungkinkan akses yang sangat ditik ke kasus jenis nilai tanpa tinju selama pencocokan pola. Pola ini memerlukan:
- Properti
HasValuejenisboolyang mengembalikantruejikaValuebukannull. - Metode
TryGetValueuntuk setiap jenis kasus yang mengembalikanbooldan memberikan nilai melaluioutparameter.TryGetValuetruemengembalikan hanya ketikaValueadalah nilai bukan null dari jenis kasus tersebut. Jenisoutparameter adalah identity-convertible ke jenis kasus, atau ke jenis nilai yang mendasar ketika jenis kasus adalah jenis nilai nullable.
[System.Runtime.CompilerServices.Union]
public struct IntOrBool : System.Runtime.CompilerServices.IUnion
{
private readonly int _intValue;
private readonly bool _boolValue;
private readonly byte _tag; // 0 = none, 1 = int, 2 = bool
public IntOrBool(int? value)
{
if (value.HasValue)
{
_intValue = value.Value;
_tag = 1;
}
}
public IntOrBool(bool? value)
{
if (value.HasValue)
{
_boolValue = value.Value;
_tag = 2;
}
}
public object? Value => _tag switch
{
1 => _intValue,
2 => _boolValue,
_ => null
};
public bool HasValue => _tag != 0;
public bool TryGetValue(out int value)
{
value = _intValue;
return _tag == 1;
}
public bool TryGetValue(out bool value)
{
value = _boolValue;
return _tag == 2;
}
}
static void NonBoxingExample()
{
IntOrBool val = new IntOrBool((int?)42);
var description = val switch
{
int i => $"int: {i}",
bool b => $"bool: {b}",
};
Console.WriteLine(description); // output: int: 42
}
Untuk pencocokan pola, pengkompilasi memanggil TryGetValue pemeriksaan jenis pola (seperti union is T) dan HasValue untuk pemeriksaan pola null (seperti union is null), menghindari tinju untuk kasus jenis nilai. Setiap anggota berlaku untuk jenis polanya sendiri—tidak ada fallback untuk yang lain. Ketika anggota hilang, pengkompilasi memeriksa properti untuk pola tersebut Value sebagai gantinya. Untuk informasi selengkapnya, lihat Bagaimana pengkompilasi menghasilkan kode untuk pencocokan pola.
Penyedia anggota gabungan
Jenis serikat dapat mendelegasikan anggota gabungannya ke antarmuka berlapis IUnionMembers publik yang dikandung dan diterapkannya secara langsung. Antarmuka tersebut adalah penyedia anggota serikat. Pengkompilasi menemukan anggota gabungan hanya pada antarmuka penyedia, termasuk anggota yang diwariskan. Anggota yang dinyatakan hanya pada jenis serikat itu sendiri tidak digunakan sebagai anggota serikat.
Saat menggunakan penyedia anggota serikat, antarmuka harus mendeklarasikan anggota yang diperlukan ini:
-
Metode statis
Create: Satu untuk setiap jenis kasus, dengan parameter tunggal dan jenis pengembalian yang dapat dikonversi identitas ke jenis serikat. Metode ini menetapkan jenis kasus Anda. -
Valueproperti: Publikobjectatauobject?properti dengangetaksesor yang mengembalikan nilai yang terkandung.
Anda dapat secara opsional mendeklarasikan anggota ini pada antarmuka sehingga pengkompilasi menghasilkan kode pencocokan pola yang lebih efisien:
-
TryGetValuemetode: Satu untuk setiap jenis kasus, dikembalikanbooldenganoutparameter jenis kasus. -
HasValueproperti: Properti publikbooldengan aksesor yanggetkembalitrueketikaValuebukannull.
Bagaimana pengkompilasi menghasilkan kode untuk pencocokan pola: TryGetValue dan HasValue berlaku untuk berbagai jenis pola—tidak ada yang mundur untuk yang lain:
- Untuk pola jenis, seperti
union is T, pengkompilasi memanggilTryGetValue(out T value)ketika dideklarasikan, mengekstrak nilai dan menerapkan pola ke dalamnya tanpa tinju. Jika tidak, pengkompilasi menerapkan pola keValueproperti , yang melakukan pemeriksaanTjenis runtime dan dapat mencentang nilai ketikaTadalah jenis nilai. - Untuk pola null, seperti
union is null, pengkompilasi memanggilHasValueketika dinyatakan untuk menguji apakah serikat berisi nilai. Jika tidak, pengkompilasi menerapkan pola null ke properti secaraValuelangsung.
Setiap anggota secara independen opsional: tanpa TryGetValue, pola jenis menggunakan Value; tanpa HasValue, pola null menggunakan Value. Mendeklarasikan salah satu anggota memungkinkan pengkompilasi menghasilkan kode yang lebih efisien dan diketik dengan kuat untuk jenis pola tersebut:
[System.Runtime.CompilerServices.Union]
public struct Outcome<T> : Outcome<T>.IUnionMembers
{
private readonly object? _value;
private Outcome(object? value) => _value = value;
public interface IUnionMembers
{
static Outcome<T> Create(T? value) => new(value);
static Outcome<T> Create(Exception? value) => new(value);
object? Value { get; }
// Optional but recommended: TryGetValue enables efficient pattern matching
bool TryGetValue(out T value);
bool TryGetValue(out Exception value);
}
object? IUnionMembers.Value => _value;
public bool TryGetValue(out T value)
{
if (_value is T t)
{
value = t;
return true;
}
value = default!;
return false;
}
public bool TryGetValue(out Exception value)
{
if (_value is Exception e)
{
value = e;
return true;
}
value = default!;
return false;
}
}
Penyedia anggota serikat pekerja berguna ketika jenis serikat pekerja membutuhkan konstruktor privat atau ketika logika pembuatan memerlukan pola pabrik, seperti dengan record class jenis gabungan.
Jenis serikat berbasis kelas
Kelas juga dapat menjadi jenis serikat. Jenis penyatuan ini berguna ketika Anda memerlukan semantik referensi atau warisan:
[System.Runtime.CompilerServices.Union]
public class Result<T> : System.Runtime.CompilerServices.IUnion
{
private readonly object? _value;
public Result(T? value) { _value = value; }
public Result(Exception? value) { _value = value; }
public object? Value => _value;
}
static void ClassUnionExample()
{
Result<string> ok = new Result<string>("success");
Result<string> err = new Result<string>(new InvalidOperationException("failed"));
Console.WriteLine(Describe(ok)); // output: OK: success
Console.WriteLine(Describe(err)); // output: Error: failed
static string Describe(Result<string> result) => result switch
{
string s => $"OK: {s}",
Exception e => $"Error: {e.Message}",
null => "null",
};
}
Untuk serikat berbasis kelas, null pola cocok dengan referensi null dan null Value.
Spesifikasi bahasa C#
Untuk informasi selengkapnya, lihat spesifikasi fitur Unions .