Add elementAtOrNull extensions (#217)

Add extensions on List and Iterable to read an element without risk of
exceptions.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f36e987..ea92e11 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,6 @@
-## 1.16.1-dev
+## 1.17.0-dev
+
+* Add `Iterable.elementAtOrNull` and `List.elementAtOrNull` extension methods.
 
 * Add a top-level `lastBy()` function that converts an `Iterable` to a `Map` by
   grouping its elements using a function, keeping the last element for each
diff --git a/lib/src/iterable_extensions.dart b/lib/src/iterable_extensions.dart
index 88f320d..3af5124 100644
--- a/lib/src/iterable_extensions.dart
+++ b/lib/src/iterable_extensions.dart
@@ -354,6 +354,17 @@
     return null;
   }
 
+  /// The [index]th element, or `null` if there is no such element.
+  ///
+  /// Returns the element at position [index] of this iterable,
+  /// just like [elementAt], if this iterable has such an element.
+  /// If this iterable does not have enough elements to have one with the given
+  /// [index], the `null` value is returned, unlike [elementAt] which throws
+  /// instead.
+  ///
+  /// The [index] must not be negative.
+  T? elementAtOrNull(int index) => skip(index).firstOrNull;
+
   /// Associates the elements in [this] by the value returned by [key].
   ///
   /// Returns a map from keys computed by [key] to the last value for which [key]
diff --git a/lib/src/list_extensions.dart b/lib/src/list_extensions.dart
index 4d6ae3f..4bb719a 100644
--- a/lib/src/list_extensions.dart
+++ b/lib/src/list_extensions.dart
@@ -259,6 +259,17 @@
     return true;
   }
 
+  /// The [index]th element, or `null` if there is no such element.
+  ///
+  /// Returns the element at position [index] of this list,
+  /// just like [elementAt], if this list has such an element.
+  /// If this list does not have enough elements to have one with the given
+  /// [index], the `null` value is returned, unlike [elementAt] which throws
+  /// instead.
+  ///
+  /// The [index] must not be negative.
+  E? elementAtOrNull(int index) => (index < length) ? this[index] : null;
+
   /// Contiguous [slice]s of [this] with the given [length].
   ///
   /// Each slice is a view of this list [length] elements long, except for the
diff --git a/pubspec.yaml b/pubspec.yaml
index 436a1e1..07a5fa1 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: collection
-version: 1.16.1-dev
+version: 1.17.0-dev
 
 description: Collections and utilities functions and classes related to collections.
 repository: https://github.com/dart-lang/collection
diff --git a/test/extensions_test.dart b/test/extensions_test.dart
index ce4b2e9..31bb458 100644
--- a/test/extensions_test.dart
+++ b/test/extensions_test.dart
@@ -1160,6 +1160,21 @@
         }
       });
     });
+    group('.elementAtOrNull', () {
+      test('empty', () async {
+        expect(iterable([]).elementAtOrNull(0), isNull);
+      });
+      test('negative index', () async {
+        expect(() => iterable([1]).elementAtOrNull(-1),
+            throwsA(isA<RangeError>()));
+      });
+      test('index within range', () async {
+        expect(iterable([1]).elementAtOrNull(0), 1);
+      });
+      test('index too high', () async {
+        expect(iterable([1]).elementAtOrNull(1), isNull);
+      });
+    });
     group('.slices', () {
       test('empty', () {
         expect(iterable(<int>[]).slices(1), []);
@@ -1748,6 +1763,20 @@
               ['1', 'b']);
         });
       });
+      group('.elementAtOrNull', () {
+        test('empty', () async {
+          expect([].elementAtOrNull(0), isNull);
+        });
+        test('negative index', () async {
+          expect(() => [1].elementAtOrNull(-1), throwsA(isA<RangeError>()));
+        });
+        test('index within range', () async {
+          expect([1].elementAtOrNull(0), 1);
+        });
+        test('index too high', () async {
+          expect([1].elementAtOrNull(1), isNull);
+        });
+      });
       group('.slices', () {
         test('empty', () {
           expect(<int>[].slices(1), []);