ReSharper memperingatkan: "Bidang statis dalam tipe generik"

261
public class EnumRouteConstraint<T> : IRouteConstraint
    where T : struct
{
    private static readonly Lazy<HashSet<string>> _enumNames; // <--

    static EnumRouteConstraint()
    {
        if (!typeof(T).IsEnum)
        {
            throw new ArgumentException(
                Resources.Error.EnumRouteConstraint.FormatWith(typeof(T).FullName));
        }

        string[] names = Enum.GetNames(typeof(T));
        _enumNames = new Lazy<HashSet<string>>(() => new HashSet<string>
        (
            names.Select(name => name), StringComparer.InvariantCultureIgnoreCase
        ));
    }

    public bool Match(HttpContextBase httpContext, Route route, 
                        string parameterName, RouteValueDictionary values, 
                        RouteDirection routeDirection)
    {
        bool match = _enumNames.Value.Contains(values[parameterName].ToString());
        return match;
    }
}

Apakah ini salah? Saya akan berasumsi bahwa ini sebenarnya memiliki static readonlybidang untuk setiap kemungkinan EnumRouteConstraint<T>yang kebetulan saya contoh.

bevacqua
sumber
Terkadang ini fitur, terkadang jengkel. Saya berharap C # memiliki beberapa kata kunci untuk membedakan mereka
nawfal

Jawaban:

468

Tidak masalah untuk memiliki bidang statis dalam tipe generik, selama Anda tahu bahwa Anda benar-benar akan mendapatkan satu bidang per kombinasi argumen tipe. Dugaan saya adalah bahwa R # hanya memperingatkan Anda jika Anda tidak menyadarinya.

Berikut ini contohnya:

using System;

public class Generic<T>
{
    // Of course we wouldn't normally have public fields, but...
    public static int Foo;
}

public class Test
{
    public static void Main()
    {
        Generic<string>.Foo = 20;
        Generic<object>.Foo = 10;
        Console.WriteLine(Generic<string>.Foo); // 20
    }
}

Seperti yang Anda lihat, Generic<string>.Fooadalah bidang yang berbeda dari Generic<object>.Foo- mereka memegang nilai yang terpisah.

Jon Skeet
sumber
Apakah ini juga berlaku ketika kelas generik mewarisi dari kelas non-generik yang berisi tipe statis. Sebagai contoh jika saya membuat yang class BaseFoomengandung anggota statis, maka berasal dari itu class Foo<T>: BaseFoosemua Foo<T>kelas akan berbagi nilai anggota statis yang sama?
bikeman868
2
Menjawab komentar saya sendiri di sini, tapi ya, semua Foo <T> akan memiliki nilai statis yang sama jika terkandung dalam kelas dasar non-generik. Lihat dotnetfiddle.net/Wz75ya
bikeman868
147

Dari wiki JetBrains :

Dalam sebagian besar kasus, memiliki bidang statis dalam tipe generik adalah tanda kesalahan. Alasan untuk ini adalah bahwa bidang statis dalam tipe generik tidak akan dibagikan di antara contoh dari berbagai jenis konstruksi dekat. Ini berarti bahwa untuk kelas generik C<T>yang memiliki bidang statis X, nilai C<int>.Xdan C<string>.X memiliki nilai independen yang sama sekali berbeda.

Dalam kasus yang jarang terjadi ketika Anda melakukan membutuhkan 'khusus' bidang statis, jangan ragu untuk menekan peringatan.

Jika Anda perlu memiliki bidang statis dibagi antara instance dengan argumen generik yang berbeda, tentukan kelas dasar non-generik untuk menyimpan anggota statis Anda, lalu atur tipe generik Anda untuk mewarisi dari tipe ini.

AakashM
sumber
13
Saat menggunakan jenis generik, secara teknis Anda berakhir dengan kelas yang berbeda dan terpisah untuk setiap jenis generik yang Anda hosting. Ketika mendeklarasikan dua kelas non-generik yang terpisah, Anda tidak akan berharap untuk berbagi variabel statis di antara mereka, jadi mengapa generik berbeda? Satu-satunya cara ini dapat dianggap langka adalah jika mayoritas pengembang tidak mengerti apa yang mereka lakukan saat membuat kelas generik.
Syndog
2
@ Syndog perilaku statika yang dijelaskan dalam kelas generik terlihat baik dan dapat dimengerti bagi saya. Tapi saya kira alasan di balik peringatan itu adalah bahwa tidak setiap tim hanya memiliki pengembang yang berpengalaman dan fokus. Kode yang benar menjadi rawan kesalahan karena kualifikasi pengembang.
Stas Ivanov
Tetapi bagaimana jika saya tidak ingin membuat kelas dasar non-generik hanya untuk menahan bidang statis ini. Bisakah saya menekan peringatan, dalam hal ini?
Tom Lint
@ Tomintint jika Anda tahu apa yang Anda lakukan maka menekan peringatan memang hal yang harus dilakukan.
AakashM
65

Ini tidak selalu merupakan kesalahan - ini memperingatkan Anda tentang kesalahpahaman potensial dari C # generics.

Cara termudah untuk mengingat apa yang dilakukan generik adalah sebagai berikut: Generik adalah "cetak biru" untuk membuat kelas, seperti halnya kelas adalah "cetak biru" untuk membuat objek. (Yah, ini adalah penyederhanaan. Anda dapat menggunakan metode generik juga.)

Dari sudut pandang MyClassRecipe<T>ini bukan kelas - itu adalah resep, cetak biru, seperti apa kelas Anda nantinya. Setelah Anda mengganti T dengan sesuatu yang konkret, katakan int, string, dll., Anda mendapatkan kelas. Adalah sah untuk memiliki anggota statis (bidang, properti, metode) yang dideklarasikan di kelas Anda yang baru dibuat (seperti di kelas lain) dan tidak ada tanda kesalahan apa pun di sini. Akan terlihat mencurigakan, pada pandangan pertama, jika Anda mendeklarasikan static MyStaticProperty<T> Property { get; set; }dalam cetak biru kelas Anda, tetapi ini juga legal. Properti Anda akan menjadi parameter, atau juga templated.

Tidak heran dalam statika VB disebut shared. Namun dalam hal ini, Anda harus menyadari bahwa anggota yang "dibagikan" tersebut hanya dibagikan di antara instance dari kelas yang sama persis, dan tidak di antara kelas berbeda yang dihasilkan dengan mengganti <T>dengan yang lain.

Alexander Christov
sumber
1
Saya pikir nama C ++ membuatnya paling jelas dari semuanya. Dalam C ++ mereka disebut Template, yang adalah mereka, Template untuk kelas konkret.
Michael Brown
8

Sudah ada beberapa jawaban bagus di sini, yang menjelaskan peringatan dan alasannya. Beberapa dari keadaan ini seperti memiliki medan statis dalam tipe generik umumnya kesalahan .

Saya pikir saya akan menambahkan contoh bagaimana fitur ini dapat berguna, yaitu kasus di mana menekan peringatan R # masuk akal.

Bayangkan Anda memiliki seperangkat kelas entitas yang ingin Anda buat bersambung, katakanlah ke Xml. Anda dapat membuat serializer untuk penggunaan ini new XmlSerializerFactory().CreateSerializer(typeof(SomeClass)), tetapi kemudian Anda harus membuat serializer terpisah untuk setiap jenis. Menggunakan generik, Anda bisa menggantinya dengan yang berikut ini, yang bisa Anda tempatkan di kelas generik tempat entitas dapat berasal:

new XmlSerializerFactory().CreateSerializer(typeof(T))

Karena Anda mungkin tidak ingin membuat serializer baru setiap kali Anda perlu membuat cerita bersambung dari jenis tertentu, Anda dapat menambahkan ini:

public class SerializableEntity<T>
{
    // ReSharper disable once StaticMemberInGenericType
    private static XmlSerializer _typeSpecificSerializer;

    private static XmlSerializer TypeSpecificSerializer
    {
        get
        {
            // Only create an instance the first time. In practice, 
            // that will mean once for each variation of T that is used,
            // as each will cause a new class to be created.
            if ((_typeSpecificSerializer == null))
            {
                _typeSpecificSerializer = 
                    new XmlSerializerFactory().CreateSerializer(typeof(T));
            }

            return _typeSpecificSerializer;
        }
    }

    public virtual string Serialize()
    {
        // .... prepare for serializing...

        // Access _typeSpecificSerializer via the property, 
        // and call the Serialize method, which depends on 
        // the specific type T of "this":
        TypeSpecificSerializer.Serialize(xmlWriter, this);
     }
}

Jika kelas ini TIDAK generik, maka setiap instance dari kelas akan menggunakan yang sama _typeSpecificSerializer.

Karena IS generik, satu set instance dengan tipe yang sama untuk Takan membagikan satu instance _typeSpecificSerializer(yang akan dibuat untuk tipe spesifik itu), sedangkan instance dengan tipe berbeda untuk Takan menggunakan instance berbeda dari _typeSpecificSerializer.

Sebuah contoh

Asalkan dua kelas yang memperpanjang SerializableEntity<T>:

// Note that T is MyFirstEntity
public class MyFirstEntity : SerializableEntity<MyFirstEntity>
{
    public string SomeValue { get; set; }
}

// Note that T is OtherEntity
public class OtherEntity : SerializableEntity<OtherEntity >
{
    public int OtherValue { get; set; }
}

... mari kita gunakan:

var firstInst = new MyFirstEntity{ SomeValue = "Foo" };
var secondInst = new MyFirstEntity{ SomeValue = "Bar" };

var thirdInst = new OtherEntity { OtherValue = 123 };
var fourthInst = new OtherEntity { OtherValue = 456 };

var xmlData1 = firstInst.Serialize();
var xmlData2 = secondInst.Serialize();
var xmlData3 = thirdInst.Serialize();
var xmlData4 = fourthInst.Serialize();

Dalam hal ini, di bawah tenda, firstInstdan secondInstakan menjadi instance dari kelas yang sama (yaitu SerializableEntity<MyFirstEntity>), dan dengan demikian, mereka akan berbagi instance dari _typeSpecificSerializer.

thirdInstdan fourthInstmerupakan instance dari kelas yang berbeda ( SerializableEntity<OtherEntity>), dan juga akan berbagi contoh _typeSpecificSerializeryang berbeda dari dua lainnya.

Ini berarti Anda mendapatkan instans serializer yang berbeda untuk masing-masing jenis entitas Anda , sambil tetap membuatnya statis dalam konteks masing-masing jenis aktual (yaitu, dibagikan di antara instance yang dari jenis tertentu).

Kjartan
sumber
Karena aturan untuk inisialisasi statis (penginisialisasi statis tidak dipanggil sampai kelas pertama kali direferensikan), Anda bisa melupakan pemeriksaan di Getter dan hanya menginisialisasi dalam deklarasi instance statis.
Michael Brown