Ошибка компилятора C # с явными переменными типа, отправленными в метод с объектами ref (преобразование из VB.net )

#c# #vb.net #type-conversion #implicit-conversion

#c# #vb.net #преобразование типа #неявное преобразование

Вопрос:

Программист с большим стажем, новичок в C #. Я нахожусь в процессе преобразования решения из VB.net в C #. Эта конкретная функция «getdata» возвращает значения из первой строки в sql select. Для этого примера я упростил код.

Из-за того, что неизвестные типы данных извлекаются из sql, параметры «getdata()» являются объектами. VB позволяет вызывать функцию с любым явным типом данных, используя параметры ref в объекты, поэтому я могу отправить string или int parm в объект и вернуть его без проблем.

В C # этот метод работает для передачи параметров по значению. Любой тип byref (ref / in / out) компилятор выдает ошибку «невозможно преобразовать из строки ref в объект ref»

Что я пробовал:

  • Изменение всех типов parm ref / var (вход / выход / ссылка / ничего)
  • Изменение типов данных переменных с явных на object работает, но создает множество проблем ниже по потоку. ie. Я не хочу определять все как объект / я предпочитаю явные типы данных.
  • явное приведение перед вызовом с использованием (object) перед именем переменной.
  • Изменение всего на динамические типы работает, но возникают те же проблемы, что и у object.

К сожалению, мое лучшее решение изменяет функциональность настолько, что это вызовет проблемы с дальнейшим преобразованием решения. Я придумал возвращать переменные a / b / c в виде анонимного объекта, которые устанавливаются в фактические переменные при возврате к вызывающей функции.

Есть ли какой-либо способ, которым параметры вызывающей функции могут быть явно введены и переданы неявному типу данных, такому как object? Если нет, есть ли лучшее решение, чем возврат анонимного типа?

Код VB — рабочий

 
    Private Sub test() 

        Dim a$, b%, c$
        getdata(1, a, b, c)
        MsgBox($"a={a}, b={b}, c={c}")

        Dim x As DateTime, y As String, z As String
        getdata(2, x, y, z)
        MsgBox($"x={x}, y={y}, z={z}")

    End Sub



    Private Sub getdata(opt As Integer, ByRef val0 As Object, ByRef Optional val1 As Object = Nothing, ByRef Optional val2 As Object = Nothing) As Boolean

       'the real implementation of this function will accept sql string and return first row of data columns
       'since fetched data will be of different types, parms are defined as objects

        If opt = 1 Then
            val0 = "Apples"
            val1 = 2
            val2 = "Oranges"
        ElseIf opt = 2 Then
            val0 = now
            val1 = "Dogs"
            val2 = "Cats"
        End If

    End Function

  

Код C # — ошибка компилятора —
Я вручную конвертирую код VB, чтобы помочь с кривой обучения C #, но моим последним решением было использовать конвертер VB-> C #, который создается здесь.

 
    private void test()
        {

            string a = null;
            int b = 0;
            string c = null;
            getdata(1, ref a, ref b, ref c);            ************** error occurs here
            MessageBox.Show($"a={a}, b={b}, c={c}");        "cannot convert from ref string to ref object"

            DateTime x = default(DateTime);
            string y = null;
            string z = null;
            getdata(2, ref x, ref y, ref z);            ************** error occurs here
            MessageBox.Show($"x={x}, y={y}, z={z}");        "cannot convert from ref string to ref object"

        }

        private bool getdata(int opt, ref object val0, ref object val1, ref object val2)
        {
            //real function will accept sql string and return first row of data columns
            //since fetched data will be of different types, parms are defined as objects
            if (opt == 1)
            {
                val0 = "Apples";
                val1 = 2;
                val2 = "Oranges";
            }
            else if (opt == 2)
            {
                val0 = DateTime.Now;
                val1 = "Dogs";
                val2 = "Cats";
            }
            return true;
        }

  

Комментарии:

1. Вам нужны x.toString(), y.toString(), z.toString()

2. @jdweng: Основные ошибки возникают при вызовах getdata.

3. Если возвращаемые результаты всегда интерполируются в строку, подобную примеру MessageBox.Show , тогда аргументы могут быть изменены с object на string . Для достижения этого присваивания всегда нужно было бы преобразовывать значения в строки.

4. Если типы данных известны во время компиляции, и они всегда будут одинаковыми для данного вызова, метод может быть преобразован в универсальный метод, т. Е. bool get data<T0, T1, T2>(int opt, ref T0 val0, ref T1 val1, ref T2 val2) .

5. Кроме того, если у вас есть подобная функция, которая принимает строки SQL, ДОЛЖЕН также существовать механизм для принятия значений входных параметров. В противном случае вы заставляете себя создавать SQL сумасшедшими способами, уязвимыми для проблем с внедрением SQL.

Ответ №1:

В этом методе есть некоторые фундаментальные вещи, которые заставляют меня поверить, что вам следует потратить больше времени на рефакторинг, чем на прямой перевод. Восстановление безопасности типов является одним из них (VB.Net упростил скрытие некоторых плохих вариантов безопасности типов в файле, где у вас есть Option Strict Off для пары модулей), но это также ДЕЙСТВИТЕЛЬНО пугает меня:


// реальная функция примет строку sql


Подобные функции, как правило, вызывают ОГРОМНЫЕ проблемы с безопасностью, а также другие проблемы, особенно когда у вас также есть куча аргументов для выходных значений. Если вы не очень хорошо разбираетесь в SQL-инъекциях, СЕЙЧАС самое время узнать об этом. Вы должны также предоставить способ включения входных данных для команды SQL, которые полностью отделены от самой строки SQL, или вы в конечном итоге окажетесь в большой беде.

Этот код нуждается в серьезном рефакторинге, а не просто в преобразовании!

Я предлагаю провести рефакторинг вокруг такого метода, как этот:

 public class DB
{
    private static string ConnectionString {get;} = "connection string here";

    private static IEnumerable<IDataRecord> getdata(string sql, Action<SqlParameterCollection> addParameters)
    {
        using (var cn = new SqlConnection(ConnectionString))
        using (var cmd = new SqlCommand(sql, cn))
        {
            addParameters(cmd.Parameters);

            cn.Open();
            using (var rdr = cmd.ExecuteReader())
            {
                while (rdr.Read())
                    yield return rdr;
                rdr.Close();
            }
        }
    }
}
  

Обратите внимание, что метод является частным; это потому, что мы еще не закончили создание класса. Как только вы создадите этот класс и удалите старый getdata() метод, везде, которые в настоящее время вызывают этот метод, появится ошибка компилятора. Это хорошо; это дает вам простой способ найти все те места, где у вас был плохой код этого типа.

Итак, теперь мы начинаем рассматривать новые ошибки компилятора. Каждый из них будет представлять место, где вы раньше вызывали getdata() . Вероятно, поблизости есть другой код для создания строки SQL. Вы хотите переместить каждый из этих разделов в новый статический метод в DB классе.

Один из этих методов может выглядеть примерно так:

 public static IDataRecord MyNewDataMethod(int ID)
{
    string SQL = "SELECT ... WHERE ID = @ID";

    return getdata(SQL, p => {
        p.Add("@ID", SqlDbType.Int).Value = ID;
    }).FirstOrDefault();
}
  

Но мы можем (и должны) сделать этот шаг дальше. Как правило, эти результаты будут представлять объекты некоторого типа. В конце концов, они должны были поступать из таблицы или, по крайней мере, набора связанных таблиц. Если у вас еще нет класса для каждой из этих вещей, вам, вероятно, следует. У этих классов должны быть статические методы с именем что-то вроде FromDataRecord() , которые принимают IDataRecord или DataRow в качестве входных данных и возвращают тип класса в качестве выходных данных. Это заводские методы. И теперь мы обновляем методы, чтобы они выглядели более похоже на это:

 public static MyObjectType MyNewDataMethod(int MyObjectTypeID)
{
    string SQL = "SELECT ... WHERE ID = @ID";

    return getdata(SQL, p => {
        p.Add("@ID", SqlDbType.Int).Value = MyObjectTypeID;
    }).Select(MyObjectType.FromDataRecord).FirstOrDefault();
}
  

Вот еще один пример, который может возвращать несколько записей:

 public static IEnumerable<MyObjectType> MyNewDataMethod(string SearchKey)
{
    string SQL = "SELECT ... WHERE SearchColumn = @SearchKey   '%'";

    return getdata(SQL, p => {
        p.Add("@SearchKey", SqlDbType.NVarChar, 80).Value = SearchKey;
    }).Select(MyObjectType.FromDataRecord);
}
  

Если вы обнаружите, что у вас есть много таких методов, вы можете преобразовать частный getdata() метод в защищенный, поместить его в собственный проект библиотеки классов в решении и использовать отдельные общедоступные классы в том же проекте, которые все еще могут обращаться к этому методу, чтобы разделить доступ к данным на логические области.

Комментарии:

1. Я думаю, что я бы реорганизовал его, чтобы использовать Dapper, а не переопределять Dapper..

2. Мы знаем, что у нас проблемы с внедрением sql. Этот проект был первоначально разработан в vb3 много лун назад. Ресурсы — это проблема, но у нас есть перегрузка для параметризованного запроса, а не для прямого sql.

3. СЕЙЧАС самое время закрыть эту проблему навсегда. Единственное место, где вы принимаете строку SQL, — это метод частного класса. Код, который создает эти строки, должен быть перемещен, чтобы стать членами этого класса; ограничьте это минимально возможным объемом кода. Данные, необходимые для завершения строк, становятся строго типизированными входными аргументами для метода. Результаты являются возвращаемыми значениями. Это упрощает проверку правильности обработки SQL методами.

Ответ №2:

Я согласен с мнением Джоэла; выбросьте этот код, а не пытайтесь его спасти. Это мусор.

Если вы добавите ссылку на Nuget package Dapper, ваша жизнь станет намного проще. С помощью Dapper вы пишете SQL, и он отображает объекты для вас. Это выглядит примерно так:

 using(var c = new SqlConnection(connection_string_here){

   var person = c.QueryFirst<(string Na, string Ad, int Ag)>("SELECT name, address, age FORM person WHERE id = @id", new { id = 123 });

}
  

В этом много чего происходит, поэтому я распакую это:

  • Первая строка просто создает соединение базы данных в using, поэтому оно будет удалено. Вам не нужно беспокоиться ни о чем другом; Dapper откроет соединение, использует его, закроет

  • Вторая строка содержит несколько частей:

  • var person = — как Dim x = 1 в VB, var объявляет переменную, тип которой определяется компилятором из любого типа, находящегося с правой стороны

  • c.QueryFirst<(string Na, string Ad, int Ag)> — QueryFirst — это метод расширения Dapper, который запускает запрос select и извлекает первую строку. Dapper сопоставляет столбцы запроса с типом, который вы указываете в угловых скобках. Здесь я привел ValueTuple, который является способом заставить компилятор C # «подделать» класс для вас на основе класса ValueTuple. Обсуждение того, как это работает, немного выходит за рамки, но достаточно сказать, что когда компилятор сталкивается с (string X, string Y, int Z) , он преобразуется за кулисами во что-то, на что вы можете ссылаться как на объект с этими именованными / типизированными свойствами. Достаточно сказать, что когда все будет сделано, вы сможете сказать person.Na или person.Ad в своем коде

  • "SELECT name, address, age FORM person WHERE id = @id" — это параметризованный SQL. Он ищет человека с некоторым идентификатором и извлекает их данные в таком порядке, имя, адрес, возраст. Порядок в этом случае важен, потому что AFIAWA dapper отображает ValueTuples позиционно, а не по имени. Это отличается от других вещей (пример позже), где он отображает по имени. Кортеж имеет имя / адрес / возраст, поэтому запрос извлекает их в том же порядке

  • new { id = 123 } — создает анонимный тип C #, своего рода класс, созданный только внутренним компилятором (отличный от valuetuple), у которого нет имени, но есть свойство, вызываемое id со значением 123. Dapper просканирует вашу строку SQL в поисках параметров и найдет вызываемый @id , поэтому он извлечет значение 123 из id свойства предоставленного анонимного типа (на этот раз на основе имени, а не позиционного)

Если у вас есть класс Person, лежащий вокруг, как вы, вероятно, должны выполнять, если вы выполняете какой-либо разумный объем работы с базой данных в c # и обратно, тогда вызов может выглядеть следующим образом:

 class Person{
    public string Name {get; set;}
    public string Address {get; set;}
    public int Age {get; set;}
}

...

    c.QueryFirst<Person>("SELECT age, name, address FROM ... WHERE id = @i", new { i=123 });

  

На этот раз мы передаем полный класс Person — Dapper отобразит объекты по имени, поэтому они находятся в другом порядке в SQL (это может быть даже SELECT * , и dapper просто проигнорирует более 10 столбцов в нашей таблице person, которые не представлены свойством класса), и это все еще работает. Если ваши имена SQL не соответствуют именам ваших классов, самое простое, что можно сделать, это присвоить им псевдонимы в SQL:

 c.QueryFirst<Person>("SELECT firstname ' ' lastname as name, ... FROM ... WHERE id = @i", new { i=123 });
  

Комментарии:

1. Dapper хорош и все такое. На самом деле мне это очень нравится (проголосовать). Но это все еще дополнительные выделения для сопоставления входных параметров и все еще требует отражения (медленнее) для отображения возвращаемых данных. Также по-прежнему рекомендуется размещать код, который использует, в своей отдельной области (либо класс для небольших программ, пространство имен для средних, либо библиотека классов для больших). Итак, на самом деле, мы не так уж много получили от нового метода getdata() в моем ответе.

2. Не может быть слишком потрепанным, если он запускает StackOverflow, хотя верно? 🙂

3. dapper-tutorial.net содержит множество полезных материалов, и при размещении вопросов об этом здесь обычно рассматривается вклад одного из авторов, Марка Гравелла

Ответ №3:

Я не думаю, что есть элегантное решение — вы можете сохранить свой метод ‘getdata’ без изменений, если добавите дополнительный багаж к каждому вызову метода:

 private void test()
{
    string a = null;
    int b = 0;
    string c = null;

    object temp_a = a;
    object temp_b = b;
    object temp_c = c;
    getdata(1, ref temp_a, ref temp_b, ref temp_c);
    a = (string)temp_a;
    b = (int)temp_b;
    c = (string)temp_c;

    MessageBox.Show($"a={a}, b={b}, c={c}");
}
  

Ответ №4:

В итоге я получил следующее


 Hashtable gd = getData();

string location = (string)gd["location"];
int locationid = (int)gd["locationid"];
string frutata = (string)gd["frutata"];
  

где getData() просто создает хэш-таблицу объектов со столбцами datareader.


Моей конечной целью было создать простую вызываемую функцию без набора кода для обработки возвращаемых значений.

Dapper кажется довольно крутым, и я буду защищать. проверьте это.